public inbox for [email protected]
help / color / mirror / Atom feedimplement CAST(expr AS type FORMAT 'template')
53+ messages / 8 participants
[nested] [flat]
* implement CAST(expr AS type FORMAT 'template')
@ 2025-07-27 15:43 jian he <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: jian he @ 2025-07-27 15:43 UTC (permalink / raw)
To: pgsql-hackers
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_ye...
Attachments:
[text/x-patch] v1-0001-CAST-val-AS-type-FORMAT-template.patch (42.7K, ../../CACJufxGqm7cYQ5C65Eoh1z-f+aMdhv9_7V=NoLH_p6uuyesi6A@mail.gmail.com/2-v1-0001-CAST-val-AS-type-FORMAT-template.patch)
download | inline diff:
From 8ddb3727f6292d47cdd42e657499fbaabf77f55f Mon Sep 17 00:00:00 2001
From: jian he <[email protected]>
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
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2025-07-27 18:31 Vik Fearing <[email protected]>
parent: jian he <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: Vik Fearing @ 2025-07-27 18:31 UTC (permalink / raw)
To: jian he <[email protected]>; pgsql-hackers
On 27/07/2025 17:43, jian he wrote:
> 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.
Why does it have to be an A_const? Shouldn't any a_expr work there?
> so the attached patch is to implement
> CAST <left paren>
> <cast operand> AS <cast target>
> [ FORMAT <cast template> ]
> <right paren>
This is correct syntax. Thanks for working on it!
> 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.
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.
> 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.
The standard is strict on what types can be cast to another, but I see
no reason not to be more generic.
--
Vik Fearing
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2025-07-28 08:41 jian he <[email protected]>
parent: Vik Fearing <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: jian he @ 2025-07-28 08:41 UTC (permalink / raw)
To: Vik Fearing <[email protected]>; +Cc: pgsql-hackers
On Mon, Jul 28, 2025 at 2:31 AM Vik Fearing <[email protected]> wrote:
>
>
> On 27/07/2025 17:43, jian he wrote:
> > 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.
>
>
> 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
>
> > so the attached patch is to implement
> > CAST <left paren>
> > <cast operand> AS <cast target>
> > [ FORMAT <cast template> ]
> > <right paren>
>
> This is correct syntax. Thanks for working on it!
>
>
> 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.
>
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.
Attachments:
[text/x-patch] v2-0001-CAST-val-AS-type-FORMAT-template.patch (44.4K, ../../CACJufxF4OW=x2rCwa+ZmcgopDwGKDXha09qTfTpCj3QSTG6Y9Q@mail.gmail.com/2-v2-0001-CAST-val-AS-type-FORMAT-template.patch)
download | inline diff:
From bd4ae95df52319fd2bb50f653cc1ed49884e2ce7 Mon Sep 17 00:00:00 2001
From: jian he <[email protected]>
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=NoLH_p6uuyesi6A@mail.gmail.com
---
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(-)
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..8918adeae00 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,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 +18836,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..ba171013ee7 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,264 @@ 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;
+ Node *fmt;
+ Node *source = 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;
+ }
+
+ textType = typeidType(TEXTOID);
+ if (IsA(format, Const) && exprType(format) == UNKNOWNOID)
+ {
+ Const *con = (Const *) format;
+ Const *fmtcon = NULL;
+ 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));
+ fmt = (Node *) fmtcon;
+ }
+ else
+ {
+ if (TypeCategory(exprType(format)) != TYPCATEGORY_STRING)
+ ereport(ERROR,
+ errcode(ERRCODE_CANNOT_COERCE),
+ errmsg("FORMAT template is not string type"),
+ parser_errposition(pstate, exprLocation(format)));
+
+ if (expression_returns_set(format))
+ ereport(ERROR,
+ errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("FORMAT template expression must not return a set"),
+ parser_errposition(pstate, exprLocation(format)));
+
+ fmt = format;
+ }
+
+ 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));
+
+ if (inputTypeId == UNKNOWNOID && IsA(node, Const))
+ {
+ /*
+ * We assume here that UNKNOWN's internal representation is the same as
+ * CSTRING.
+ */
+ Const *con = (Const *) node;
+ Const *newcon = NULL;
+
+ 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));
+ }
+ source = (Node *) newcon;
+ }
+ else
+ source = node;
+
+ 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;
+ }
+
+ args = list_make1(source);
+ args = lappend(args, fmt);
+ 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..f4a3b0e1219 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,9 @@ transformTypeCast(ParseState *pstate, TypeCast *tc)
if (inputType == InvalidOid)
return expr; /* do nothing if NULL input */
+ if(tc->format)
+ format = transformExprRecurse(pstate, tc->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
@@ -2763,11 +2773,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..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)
+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..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');
-- 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
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2025-07-28 10:47 Vik Fearing <[email protected]>
parent: jian he <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: Vik Fearing @ 2025-07-28 10:47 UTC (permalink / raw)
To: jian he <[email protected]>; +Cc: pgsql-hackers
On 28/07/2025 10:41, jian he wrote:
> 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).
This is the same issue I came across when I tried to implement it
several years ago.
> 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.
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
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2025-07-29 03:13 jian he <[email protected]>
parent: Vik Fearing <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: jian he @ 2025-07-29 03:13 UTC (permalink / raw)
To: Vik Fearing <[email protected]>; +Cc: pgsql-hackers
On Mon, Jul 28, 2025 at 6:47 PM Vik Fearing <[email protected]> wrote:
>
> > 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.
>
>
> 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.
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?
Attachments:
[text/x-patch] v3-0001-CAST-val-AS-type-FORMAT-template.patch (59.2K, ../../CACJufxEH-8UPdbPoUoqNRaiOePw+s2W2DG4OpXtoSYDaW30oAg@mail.gmail.com/2-v3-0001-CAST-val-AS-type-FORMAT-template.patch)
download | inline diff:
From c944169304c3922c3cc76166ccd7a54cfcf595ba Mon Sep 17 00:00:00 2001
From: jian he <[email protected]>
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=NoLH_p6uuyesi6A@mail.gmail.com
---
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, ¶llel);
@@ -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 *
+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..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;
}
+/*
+ * 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()
@@ -545,6 +608,209 @@ coerce_type(ParseState *pstate, Node *node,
return NULL; /* keep compiler quiet */
}
+Node *
+coerce_type_fmt(ParseState *pstate, Node *node, Node *format,
+ Oid inputTypeId, Oid targetTypeId, int32 targetTypeMod,
+ CoercionContext ccontext, CoercionForm cformat, int location)
+{
+ Node *result;
+ Node *fmt;
+ Node *source = NULL;
+ Oid funcId = InvalidOid;
+ Oid baseTypeId;
+ int32 baseTypeMod;
+ Oid inputBaseTypeId;
+ FuncExpr *fexpr;
+ Type textType;
+ List *args;
+ Relation pg_proc;
+ SysScanDesc sscan;
+ ScanKeyData scankey;
+ HeapTuple tuple;
+
+ baseTypeMod = targetTypeMod;
+ baseTypeId = getBaseTypeAndTypmod(targetTypeId, &baseTypeMod);
+ inputBaseTypeId = getBaseType(inputTypeId);
+
+ if (targetTypeId == inputTypeId ||
+ node == NULL)
+ {
+ /* no conversion needed */
+ return node;
+ }
+
+ textType = typeidType(TEXTOID);
+ if (IsA(format, Const) && exprType(format) == UNKNOWNOID)
+ {
+ Const *con = (Const *) format;
+ Const *fmtcon = NULL;
+ 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));
+ fmt = (Node *) fmtcon;
+ }
+ else
+ {
+ if (TypeCategory(exprType(format)) != TYPCATEGORY_STRING)
+ ereport(ERROR,
+ errcode(ERRCODE_CANNOT_COERCE),
+ errmsg("FORMAT template is not string type"),
+ parser_errposition(pstate, exprLocation(format)));
+
+ if (expression_returns_set(format))
+ ereport(ERROR,
+ errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("FORMAT template expression must not return a set"),
+ parser_errposition(pstate, exprLocation(format)));
+
+ fmt = format;
+ }
+
+ if (inputTypeId == UNKNOWNOID && IsA(node, Const))
+ {
+ /*
+ * We assume here that UNKNOWN's internal representation is the same as
+ * CSTRING.
+ */
+ Const *con = (Const *) node;
+ Const *newcon = NULL;
+
+ 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));
+ }
+ source = (Node *) newcon;
+
+ inputBaseTypeId = TEXTOID;
+ }
+ else
+ source = node;
+
+ 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;
+ break;
+ }
+ systable_endscan(sscan);
+ table_close(pg_proc, AccessShareLock);
+
+ if (!OidIsValid(funcId))
+ {
+ if (inputTypeId == UNKNOWNOID)
+ inputTypeId = TEXTOID;
+
+ ereport(ERROR,
+ errcode(ERRCODE_CANNOT_COERCE),
+ errmsg("cannot use FORMAT template cast type %s to %s",
+ format_type_be(inputTypeId),
+ format_type_be(targetTypeId)),
+ errhint("Formatted type cast function casting %s to %s does not exists",
+ format_type_be(inputTypeId),
+ format_type_be(targetTypeId)),
+ parser_coercion_errposition(pstate, location, node));
+ }
+ 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;
+ }
+
+ args = list_make1(source);
+ args = lappend(args, fmt);
+ 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()
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index d66276801c6..ef7d103a998 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,9 @@ transformTypeCast(ParseState *pstate, TypeCast *tc)
if (inputType == InvalidOid)
return expr; /* do nothing if NULL input */
+ if(tc->format)
+ format = transformExprRecurse(pstate, tc->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
@@ -2763,11 +2773,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..bde75c45028 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,70 @@ 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;
+ 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)
+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..e26f29b23de 100644
--- a/src/test/regress/expected/misc.out
+++ b/src/test/regress/expected/misc.out
@@ -396,3 +396,172 @@ 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 use FORMAT template cast type text to text
+LINE 1: select cast('1' as text format '1'::text);
+ ^
+HINT: Formatted type cast function casting text to text does not exists
+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 use FORMAT template cast type text to timestamp without time zone
+LINE 1: select cast('1' as timestamp format 'YYYY-MM-DD');
+ ^
+HINT: Formatted type cast function casting text to timestamp without time zone does not exists
+select cast('1' as timestamp[] format 'YYYY-MM-DD'); --error
+ERROR: cannot use FORMAT template cast type text to timestamp without time zone[]
+LINE 1: select cast('1' as timestamp[] format 'YYYY-MM-DD');
+ ^
+HINT: Formatted type cast function casting text to timestamp without time zone[] does not exists
+select cast('1' as bool format 'YYYY-MM-DD'); --error
+ERROR: cannot use FORMAT template cast type text to boolean
+LINE 1: select cast('1' as bool format 'YYYY-MM-DD');
+ ^
+HINT: Formatted type cast function casting text to boolean does not exists
+select cast('1' as json format 'YYYY-MM-DD'); --error
+ERROR: cannot use FORMAT template cast type text to json
+LINE 1: select cast('1' as json format 'YYYY-MM-DD');
+ ^
+HINT: Formatted type cast function casting text to json does not exists
+select cast('1'::json as text format 'YYYY-MM-DD'); --error
+ERROR: cannot use FORMAT template cast type json to text
+LINE 1: select cast('1'::json as text format 'YYYY-MM-DD');
+ ^
+HINT: Formatted type cast function casting json to text does not exists
+--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)
+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..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');
-- 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
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2025-07-29 03:54 David G. Johnston <[email protected]>
parent: jian he <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: David G. Johnston @ 2025-07-29 03:54 UTC (permalink / raw)
To: jian he <[email protected]>; +Cc: Vik Fearing <[email protected]>; pgsql-hackers
On Monday, July 28, 2025, jian he <[email protected]> wrote:
> On Mon, Jul 28, 2025 at 6:47 PM Vik Fearing <[email protected]>
> wrote:
> >
> > > adding these pg_cast entries seems tricky.
>
> select proname, prosrc, proformat from pg_proc where proformat;
>
> what do you think?
>
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.
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2025-08-01 08:22 jian he <[email protected]>
parent: David G. Johnston <[email protected]>
0 siblings, 2 replies; 53+ messages in thread
From: jian he @ 2025-08-01 08:22 UTC (permalink / raw)
To: David G. Johnston <[email protected]>; +Cc: Vik Fearing <[email protected]>; pgsql-hackers
On Tue, Jul 29, 2025 at 11:54 AM David G. Johnston
<[email protected]> 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?
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2025-08-01 10:34 Vik Fearing <[email protected]>
parent: jian he <[email protected]>
1 sibling, 1 reply; 53+ messages in thread
From: Vik Fearing @ 2025-08-01 10:34 UTC (permalink / raw)
To: jian he <[email protected]>; David G. Johnston <[email protected]>; +Cc: pgsql-hackers
On 01/08/2025 10:22, jian he wrote:
> 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?
My fear is that if we don't, it will never get done.
--
Vik Fearing
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2025-08-04 03:10 jian he <[email protected]>
parent: Vik Fearing <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: jian he @ 2025-08-04 03:10 UTC (permalink / raw)
To: Vik Fearing <[email protected]>; +Cc: David G. Johnston <[email protected]>; pgsql-hackers
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);
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2025-08-04 03:36 David G. Johnston <[email protected]>
parent: jian he <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: David G. Johnston @ 2025-08-04 03:36 UTC (permalink / raw)
To: jian he <[email protected]>; +Cc: Vik Fearing <[email protected]>; pgsql-hackers
On Sun, Aug 3, 2025 at 8:10 PM jian he <[email protected]> wrote:
> 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);
>
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.
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2025-08-04 05:38 Corey Huinker <[email protected]>
parent: David G. Johnston <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: Corey Huinker @ 2025-08-04 05:38 UTC (permalink / raw)
To: David G. Johnston <[email protected]>; +Cc: jian he <[email protected]>; Vik Fearing <[email protected]>; pgsql-hackers
On Sun, Aug 3, 2025 at 11:36 PM David G. Johnston <
[email protected]> wrote:
> On Sun, Aug 3, 2025 at 8:10 PM jian he <[email protected]>
> wrote:
>
>> 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);
>>
>
> I'm hoping the standard says (or allows us to) error out here.
>
We have some influence in that, I believe.
>
> 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.
>
+1
> 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.
>
> 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?
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2025-08-04 05:39 Corey Huinker <[email protected]>
parent: jian he <[email protected]>
1 sibling, 0 replies; 53+ messages in thread
From: Corey Huinker @ 2025-08-04 05:39 UTC (permalink / raw)
To: jian he <[email protected]>; +Cc: David G. Johnston <[email protected]>; Vik Fearing <[email protected]>; pgsql-hackers
> 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?
>
>
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.
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2025-08-04 05:55 David G. Johnston <[email protected]>
parent: Corey Huinker <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: David G. Johnston @ 2025-08-04 05:55 UTC (permalink / raw)
To: Corey Huinker <[email protected]>; +Cc: jian he <[email protected]>; Vik Fearing <[email protected]>; pgsql-hackers
On Sunday, August 3, 2025, Corey Huinker <[email protected]> wrote:
> On Sun, Aug 3, 2025 at 11:36 PM David G. Johnston <
> [email protected]> wrote:
>
>> On Sun, Aug 3, 2025 at 8:10 PM jian he <[email protected]>
>> wrote:
>>
>>> 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);
>>>
>>
>> I'm hoping the standard says (or allows us to) error out here.
>>
>
> We have some influence in that, I believe.
>
>
>>
>> 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.
>>
>
> +1
>
>
>
>> 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.
But, once this is implemented, does it then make sense to then parse
> 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.
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2025-08-04 12:57 Vik Fearing <[email protected]>
parent: David G. Johnston <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: Vik Fearing @ 2025-08-04 12:57 UTC (permalink / raw)
To: David G. Johnston <[email protected]>; Corey Huinker <[email protected]>; +Cc: jian he <[email protected]>; pgsql-hackers
On 04/08/2025 07:55, David G. Johnston wrote:
> On Sunday, August 3, 2025, Corey Huinker <[email protected]> wrote:
>
> On Sun, Aug 3, 2025 at 11:36 PM David G. Johnston
> <[email protected]> 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.
>
>
> 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
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2025-08-12 06:37 jian he <[email protected]>
parent: Vik Fearing <[email protected]>
0 siblings, 2 replies; 53+ messages in thread
From: jian he @ 2025-08-12 06:37 UTC (permalink / raw)
To: Vik Fearing <[email protected]>; +Cc: David G. Johnston <[email protected]>; Corey Huinker <[email protected]>; pgsql-hackers
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=x2rCwa+ZmcgopDwGKDXha09qTfTpCj3QSTG6Y9Q@mail.gmail.com
Attachments:
[text/x-patch] v4-0001-CAST-val-AS-type-FORMAT-template.patch (47.3K, ../../CACJufxH+soqLj_AuMQj-_jxunVQKX-HBQA_3_3vmV1jTRyZ1hA@mail.gmail.com/2-v4-0001-CAST-val-AS-type-FORMAT-template.patch)
download | inline diff:
From ef719a5fcf2e96d7fc756a021aebd8caf5b0b385 Mon Sep 17 00:00:00 2001
From: jian he <[email protected]>
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=NoLH_p6uuyesi6A@mail.gmail.com
---
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(-)
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 db43034b9db..ef08bba36ef 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);
@@ -15933,6 +15935,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"),
@@ -18820,12 +18824,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..28be9b94637 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 type coerce don't involve format template
+ */
+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,281 @@ 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;
+ Node *fmt;
+ Node *source = NULL;
+ Oid funcId;
+ Oid targetBaseTypeId;
+ int32 baseTypeMod;
+ Oid inputBaseTypeId;
+ char t_typcategory;
+ char s_typcategory;
+ FuncExpr *fexpr;
+ Type textType;
+ List *args;
+
+ baseTypeMod = targetTypeMod;
+ targetBaseTypeId = getBaseTypeAndTypmod(targetTypeId, &baseTypeMod);
+ inputBaseTypeId = getBaseType(inputTypeId);
+ t_typcategory = TypeCategory(targetBaseTypeId);
+ s_typcategory = TypeCategory(inputTypeId);
+
+ if (targetTypeId == inputTypeId ||
+ node == NULL)
+ {
+ ereport(ERROR,
+ errcode(ERRCODE_CANNOT_COERCE),
+ errmsg("can not use FORMAT template for binary coerceable type cast"),
+ parser_errposition(pstate, exprLocation(format)));
+ }
+
+ textType = typeidType(TEXTOID);
+ if (IsA(format, Const) && exprType(format) == UNKNOWNOID)
+ {
+ Const *con = (Const *) format;
+ Const *fmtcon = NULL;
+ 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));
+ fmt = (Node *) fmtcon;
+ }
+ else
+ {
+ if (TypeCategory(exprType(format)) != TYPCATEGORY_STRING)
+ ereport(ERROR,
+ errcode(ERRCODE_CANNOT_COERCE),
+ errmsg("FORMAT template is not string type"),
+ parser_errposition(pstate, exprLocation(format)));
+
+ if (expression_returns_set(format))
+ ereport(ERROR,
+ errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("FORMAT template expression must not return a set"),
+ parser_errposition(pstate, exprLocation(format)));
+
+ fmt = format;
+ }
+
+ if (targetBaseTypeId != NUMERICOID &&
+ targetBaseTypeId != TIMESTAMPTZOID &&
+ targetBaseTypeId != DATEOID &&
+ t_typcategory != TYPCATEGORY_STRING)
+ {
+ 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 currently"),
+ 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_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot cast type %s to %s using formatted template",
+ format_type_be(inputTypeId),
+ format_type_be(targetTypeId)),
+ errhint("Only catgeory of numeric, string, datetime, and timespan source data type are supported for formatted type casting");
+ parser_coercion_errposition(pstate, location, node));
+ }
+
+ /*
+ * 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)
+ {
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot cast type %s to %s using formatted template",
+ "text",
+ format_type_be(targetTypeId)),
+ parser_coercion_errposition(pstate, location, node));
+ }
+
+ if (targetBaseTypeId == NUMERICOID)
+ funcId = fmgr_internal_function("numeric_to_number");
+ else if (targetBaseTypeId == TIMESTAMPTZOID)
+ funcId = fmgr_internal_function("to_timestamp");
+ else if (targetBaseTypeId == DATEOID)
+ funcId = fmgr_internal_function("to_date");
+ else
+ funcId = get_fmt_function(inputBaseTypeId); /* to_char variant */
+
+ Assert(OidIsValid(funcId));
+
+ if (inputTypeId == UNKNOWNOID && IsA(node, Const))
+ {
+ /*
+ * We assume here that UNKNOWN's internal representation is the same as
+ * CSTRING.
+ */
+ Const *con = (Const *) node;
+ Const *newcon = NULL;
+
+ 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));
+ }
+ source = (Node *) newcon;
+ }
+ else
+ source = node;
+
+ /* FIXME: don't understand this part */
+ 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;
+ }
+
+ args = list_make1(source);
+ args = lappend(args, fmt);
+ 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 != targetBaseTypeId)
+ result = coerce_to_domain(result, targetBaseTypeId, 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..ef7d103a998 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,9 @@ transformTypeCast(ParseState *pstate, TypeCast *tc)
if (inputType == InvalidOid)
return expr; /* do nothing if NULL input */
+ if(tc->format)
+ format = transformExprRecurse(pstate, tc->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
@@ -2763,11 +2773,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 afcf54169c3..b1c19e6b105 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 3d6e6bdbfd2..187a776d963 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,
@@ -10808,6 +10810,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);
@@ -10864,6 +10870,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..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)
+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..e7646d6a2a2 100644
--- a/src/test/regress/sql/misc.sql
+++ b/src/test/regress/sql/misc.sql
@@ -273,3 +273,54 @@ 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('1'::text as text format '1'::text); --error
+select cast(array[1] as text format 'YYYY'); --error
+select cast('1' 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' as date format 'YYYY-MM-DD'); --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
+
+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;
+
+--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
+
+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 stable_const() RETURNS TEXT AS $$ BEGIN RETURN 'YYYY-MM-DD'; END; $$ LANGUAGE plpgsql STABLE;
+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 index s1 on tcast(cast(col1 as date format stable_const())); --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);
+
+--null value check
+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::numeric 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');
+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');
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
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-03-23 20:20 Corey Huinker <[email protected]>
parent: jian he <[email protected]>
1 sibling, 1 reply; 53+ messages in thread
From: Corey Huinker @ 2026-03-23 20:20 UTC (permalink / raw)
To: zengman <[email protected]>; [email protected]; +Cc: jian he <[email protected]>; David G. Johnston <[email protected]>; pgsql-hackers; Vik Fearing <[email protected]>
>
>
> 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.
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-03-24 06:30 Zsolt Parragi <[email protected]>
parent: jian he <[email protected]>
1 sibling, 1 reply; 53+ messages in thread
From: Zsolt Parragi @ 2026-03-24 06:30 UTC (permalink / raw)
To: jian he <[email protected]>; +Cc: Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; Corey Huinker <[email protected]>; pgsql-hackers
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.
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-03-30 08:07 jian he <[email protected]>
parent: Corey Huinker <[email protected]>
0 siblings, 0 replies; 53+ messages in thread
From: jian he @ 2026-03-30 08:07 UTC (permalink / raw)
To: Corey Huinker <[email protected]>; +Cc: zengman <[email protected]>; [email protected]; David G. Johnston <[email protected]>; pgsql-hackers; Vik Fearing <[email protected]>
On Tue, Mar 24, 2026 at 4:20 AM Corey Huinker <[email protected]> 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:
>
> + 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?
>
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.
> + 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.
>
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/
Attachments:
[text/x-patch] v6-0001-CAST-expr-AS-type-FORMAT-template.patch (73.2K, ../../CACJufxENmmhW7Z99-U7ut+NNPZx34vp4K1VA-Xp2QnD-xyfneg@mail.gmail.com/2-v6-0001-CAST-expr-AS-type-FORMAT-template.patch)
download | inline diff:
From 28c00853b0fa76d08a9d0503eeccaa94feb6e2f3 Mon Sep 17 00:00:00 2001
From: jian he <[email protected]>
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=x2rCwa+ZmcgopDwGKDXha09qTfTpCj3QSTG6Y9Q@mail.gmail.com
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=NoLH_p6uuyesi6A@mail.gmail.com
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)
+{
+ TypeCast *n = makeNode(TypeCast);
+
+ n->arg = arg;
+ n->typeName = typename;
+ n->format = format;
+ 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_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
+------------------------------
+ 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
------------------------------
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('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
--
@@ -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)
+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)
--
@@ -3868,6 +3972,32 @@ 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'),
+ 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
+-------------------------
+ 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
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;
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 +706,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,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');
-- 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
--
2.34.1
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-03-30 08:09 jian he <[email protected]>
parent: Zsolt Parragi <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: jian he @ 2026-03-30 08:09 UTC (permalink / raw)
To: Zsolt Parragi <[email protected]>; +Cc: Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; Corey Huinker <[email protected]>; pgsql-hackers
On Tue, Mar 24, 2026 at 2:31 PM Zsolt Parragi <[email protected]> wrote:
> Hello!
>
> There's some inconsistency in deparse, it displays to_char for intervals:
>
> 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');
> 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.
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.
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-03-30 22:17 Zsolt Parragi <[email protected]>
parent: jian he <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: Zsolt Parragi @ 2026-03-30 22:17 UTC (permalink / raw)
To: jian he <[email protected]>; +Cc: Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; Corey Huinker <[email protected]>; pgsql-hackers
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?
+ format = coerce_to_target_type(pstate, fmt,
+ exprType(fmt), TEXTOID,
+ exprTypmod(fmt),
+ ccontext, cformat,
+ exprLocation(fmt));
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') ;
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-03-31 07:47 jian he <[email protected]>
parent: Zsolt Parragi <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: jian he @ 2026-03-31 07:47 UTC (permalink / raw)
To: Zsolt Parragi <[email protected]>; +Cc: Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; Corey Huinker <[email protected]>; pgsql-hackers
On Tue, Mar 31, 2026 at 6:18 AM Zsolt Parragi <[email protected]> wrote:
>
> 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?
>
I have removed this white spce.
> + format = coerce_to_target_type(pstate, fmt,
> + exprType(fmt), TEXTOID,
> + exprTypmod(fmt),
> + ccontext, cformat,
> + exprLocation(fmt));
>
> Is exprTypmod(fmt) correct, shouldn't it use -1?
>
yes. it should be -1.
> This one also shows a not so userfriendly error:
>
> postgres=# SELECT CAST('hello' AS name FORMAT 'test') ;
>
> 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/
Attachments:
[text/x-patch] v7-0001-CAST-expr-AS-type-FORMAT-template.patch (74.5K, ../../CACJufxH4ELUjdGNi7vkHEA6L=G0SfE5abnFs5vjvEm3G-Jqsbw@mail.gmail.com/2-v7-0001-CAST-expr-AS-type-FORMAT-template.patch)
download | inline diff:
From e07a575e81069a91ff7b9168cd4585ac5d4d10bf Mon Sep 17 00:00:00 2001
From: jian he <[email protected]>
Date: Tue, 31 Mar 2026 15:45:17 +0800
Subject: [PATCH v7 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=x2rCwa+ZmcgopDwGKDXha09qTfTpCj3QSTG6Y9Q@mail.gmail.com
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=NoLH_p6uuyesi6A@mail.gmail.com
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 | 325 ++++++++++++++++-
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 | 329 ++++++++++++++++++
.../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 | 114 ++++++
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, 1257 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)
+{
+ TypeCast *n = makeNode(TypeCast);
+
+ n->arg = arg;
+ n->typeName = typename;
+ n->format = format;
+ 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_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..70c45e011bf 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,242 @@ 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;
+ s_typcategory = TYPCATEGORY_STRING;
+ }
+
+ /*
+ * 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: errdetail 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)),
+ errdetail("Only timestamptz, text, numeric and date data type are supported for formatted type casting"),
+ parser_coercion_errposition(pstate, location, node));
+ break;
+ }
+
+ /*
+ * 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)));
+
+ /*
+ * 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, -1,
+ ccontext, cformat,
+ exprLocation(fmt));
+
+ 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 +414,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 +434,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 +459,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 +493,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 +538,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..040eabe09bd 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..e08714bed93
--- /dev/null
+++ b/src/test/regress/expected/cast.out
@@ -0,0 +1,329 @@
+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('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
+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');
+ ^
+DETAIL: 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');
+ ^
+DETAIL: 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');
+ ^
+DETAIL: 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');
+ ^
+DETAIL: 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');
+ ^
+DETAIL: 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');
+ ^
+DETAIL: 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');
+ ^
+DETAIL: 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');
+ ^
+DETAIL: 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');
+ ^
+DETAIL: 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-...
+ ^
+DETAIL: 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...
+ ^
+DETAIL: 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...
+ ^
+DETAIL: 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)
+
+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::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
+------------------------------
+ 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
------------------------------
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('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
--
@@ -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)
+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)
--
@@ -3868,6 +3972,32 @@ 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'),
+ 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
+-------------------------
+ 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
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..e7062f44e42
--- /dev/null
+++ b/src/test/regress/sql/cast.sql
@@ -0,0 +1,114 @@
+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('hello' as name format 'test');
+select cast('hello' as bpchar format 'test');
+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);
+
+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;
+
+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;
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 +706,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,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');
-- 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
--
2.34.1
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-03-31 17:47 Corey Huinker <[email protected]>
parent: jian he <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: Corey Huinker @ 2026-03-31 17:47 UTC (permalink / raw)
To: jian he <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; pgsql-hackers
On Tue, Mar 31, 2026 at 3:48 AM jian he <[email protected]> wrote:
> On Tue, Mar 31, 2026 at 6:18 AM Zsolt Parragi <[email protected]>
> wrote:
> >
> > 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?
> >
> I have removed this white spce.
>
> > + format = coerce_to_target_type(pstate, fmt,
> > + exprType(fmt), TEXTOID,
> > + exprTypmod(fmt),
> > + ccontext, cformat,
> > + exprLocation(fmt));
> >
> > Is exprTypmod(fmt) correct, shouldn't it use -1?
> >
> yes. it should be -1.
>
> > This one also shows a not so userfriendly error:
> >
> > postgres=# SELECT CAST('hello' AS name FORMAT 'test') ;
> >
> > 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/
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.
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-06-18 14:51 Robert Haas <[email protected]>
parent: Corey Huinker <[email protected]>
0 siblings, 3 replies; 53+ messages in thread
From: Robert Haas @ 2026-06-18 14:51 UTC (permalink / raw)
To: Corey Huinker <[email protected]>; +Cc: jian he <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; pgsql-hackers
On Tue, Mar 31, 2026 at 1:48 PM Corey Huinker <[email protected]> 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
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-06-29 05:19 Haibo Yan <[email protected]>
parent: Robert Haas <[email protected]>
2 siblings, 1 reply; 53+ messages in thread
From: Haibo Yan @ 2026-06-29 05:19 UTC (permalink / raw)
To: Robert Haas <[email protected]>; +Cc: Corey Huinker <[email protected]>; jian he <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; pgsql-hackers
On Thu, Jun 18, 2026 at 7:52 AM Robert Haas <[email protected]> wrote:
>
> On Tue, Mar 31, 2026 at 1:48 PM Corey Huinker <[email protected]> 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
>
>
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
Attachments:
[application/octet-stream] 0004-Add-built-in-formatters-for-CAST-FORMAT.patch (45.2K, ../../CABXr29FyPC7terFF7E+r462BEHhYgv06oUVoBrhkH7xhshuE6A@mail.gmail.com/2-0004-Add-built-in-formatters-for-CAST-FORMAT.patch)
download | inline diff:
From d98fffd98a5cb9046117555d1b88147621311d8b Mon Sep 17 00:00:00 2001
From: Haibo Yan <[email protected]>
Date: Sun, 28 Jun 2026 13:28:19 -0700
Subject: [PATCH] Add built-in formatters for CAST ... FORMAT
Add built-in formatter functions and pg_formatter bootstrap rows for the
initial datetime/string formatted casts:
text/varchar/bpchar <-> date
text/varchar/bpchar <-> timestamp
text/varchar/bpchar <-> timestamptz
The wrappers reuse PostgreSQL's existing formatting code and are reached
through pg_formatter, not by parser or analyzer special cases. They are
STRICT, so NULL input or NULL FORMAT returns NULL.
This provides SQL-standard-style CAST ... FORMAT support for the common
datetime/string cases, while keeping PostgreSQL extensions from the
generic infrastructure: FORMAT may be any expression coercible to text,
and formatter lookup remains catalog-driven. The patch does not claim
complete SQL-standard datetime-template conformance; time, timetz,
numeric, interval, and fuller template coverage are left for future work.
Built-in formatter rows are treated as system objects and are not dumped
by pg_dump.
---
src/backend/utils/adt/formatting.c | 325 ++++++++++++++++++++++
src/bin/pg_dump/pg_dump.c | 25 +-
src/include/catalog/pg_formatter.dat | 71 +++++
src/include/catalog/pg_proc.dat | 77 +++++
src/test/regress/expected/expressions.out | 25 +-
src/test/regress/expected/formatters.out | 282 ++++++++++++++++++-
src/test/regress/sql/expressions.sql | 17 +-
src/test/regress/sql/formatters.sql | 131 ++++++++-
8 files changed, 930 insertions(+), 23 deletions(-)
create mode 100644 src/include/catalog/pg_formatter.dat
diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c
index d52d71b0a8c..b1c0524a7ac 100644
--- a/src/backend/utils/adt/formatting.c
+++ b/src/backend/utils/adt/formatting.c
@@ -4138,6 +4138,331 @@ to_date(PG_FUNCTION_ARGS)
PG_RETURN_DATEADT(result);
}
+/* ----------
+ * Built-in formatters for CAST ( <operand> AS <type> FORMAT <template> )
+ *
+ * These thin wrappers provide the initial datetime/string CAST ... FORMAT
+ * built-ins. They are registered in pg_formatter (see pg_formatter.dat) and
+ * reached only through a CoerceViaFormatter node; they are not a primary
+ * user-facing API (to_char()/to_date()/to_timestamp() serve that purpose).
+ * They reuse PostgreSQL's existing formatting code rather than introducing a
+ * second template engine: to_date()/to_timestamp()/parse_datetime() for the
+ * string -> datetime direction and timestamp_to_char()/timestamptz_to_char()
+ * for the datetime -> string direction. The accepted template tokens are thus
+ * exactly those of to_char()/to_date().
+ *
+ * A separate wrapper per source/target type is needed because pg_formatter
+ * lookup matches the exact source and target types: text, varchar and bpchar
+ * are distinct on each side. On the source side text and varchar share a C
+ * symbol (identical varlena representation and return type), while bpchar gets
+ * its own symbol because the blank padding must be trimmed first. On the
+ * target side each of text, varchar and bpchar gets its own exported symbol
+ * (so no two built-in functions share a prosrc with a different return type),
+ * all delegating to a common static helper. The returned text varlena is a
+ * valid varchar/bpchar value; any declared length/typmod is enforced afterward
+ * by the ordinary coercion layered above the CoerceViaFormatter node.
+ *
+ * All of these functions are STRICT (proisstrict in pg_proc.dat), so a NULL
+ * source value or NULL FORMAT expression yields NULL without the C code ever
+ * being called, matching ordinary strict cast-function behavior.
+ * ----------
+ */
+
+/*
+ * Strip blank padding from a bpchar argument, returning a plain text value.
+ * Standard-style character casts must not fail merely because a fixed-length
+ * CHAR(n) source carries trailing spaces, so bpchar sources are right-trimmed
+ * before the format template is applied.
+ *
+ * bpchar padding uses ordinary ASCII space bytes, so we trim only trailing
+ * ' ' bytes here. This is deliberately not a general Unicode whitespace trim.
+ */
+static text *
+formatcast_rtrim_blanks(text *src)
+{
+ char *data = VARDATA_ANY(src);
+ int len = VARSIZE_ANY_EXHDR(src);
+ int orig_len = len;
+
+ while (len > 0 && data[len - 1] == ' ')
+ len--;
+
+ /* Avoid a copy when there was no trailing padding to trim. */
+ if (len == orig_len)
+ return src;
+
+ return cstring_to_text_with_len(data, len);
+}
+
+/*
+ * string -> date: reuse to_date() verbatim, so behavior matches the
+ * SQL-callable to_date(text, text).
+ */
+static Datum
+formatcast_in_date(text *src, text *fmt, Oid collid)
+{
+ return DirectFunctionCall2Coll(to_date, collid,
+ PointerGetDatum(src), PointerGetDatum(fmt));
+}
+
+/*
+ * string -> timestamp without time zone. There is no SQL-callable function
+ * for this (to_timestamp() returns timestamptz), so we parse with
+ * parse_datetime() and build the result from the local datetime fields without
+ * consulting the session time zone. A date-only template is widened to
+ * timestamp; a zoned template is rejected, since the result type carries no
+ * zone and we must not silently fold a time zone through the session setting.
+ */
+static Datum
+formatcast_in_timestamp(text *src, text *fmt, Oid collid)
+{
+ Oid typid;
+ int32 typmod;
+ int tz;
+ Datum d;
+
+ d = parse_datetime(src, fmt, collid, false, &typid, &typmod, &tz, NULL);
+
+ switch (typid)
+ {
+ case TIMESTAMPOID:
+ return d;
+ case DATEOID:
+ return TimestampGetDatum(date2timestamp_safe(DatumGetDateADT(d),
+ NULL));
+ default:
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_DATETIME_FORMAT),
+ errmsg("FORMAT template for a cast to timestamp without time zone must not specify a time zone")));
+ return (Datum) 0; /* keep compiler quiet */
+ }
+}
+
+/*
+ * string -> timestamp with time zone: reuse to_timestamp() verbatim, matching
+ * the SQL-callable to_timestamp(text, text).
+ */
+static Datum
+formatcast_in_timestamptz(text *src, text *fmt, Oid collid)
+{
+ return DirectFunctionCall2Coll(to_timestamp, collid,
+ PointerGetDatum(src), PointerGetDatum(fmt));
+}
+
+/* string -> date (text and varchar sources) */
+Datum
+formatcast_str_to_date(PG_FUNCTION_ARGS)
+{
+ return formatcast_in_date(PG_GETARG_TEXT_PP(0), PG_GETARG_TEXT_PP(1),
+ PG_GET_COLLATION());
+}
+
+/* string -> date (bpchar source; trailing blanks trimmed) */
+Datum
+formatcast_bpchar_to_date(PG_FUNCTION_ARGS)
+{
+ return formatcast_in_date(formatcast_rtrim_blanks(PG_GETARG_TEXT_PP(0)),
+ PG_GETARG_TEXT_PP(1), PG_GET_COLLATION());
+}
+
+/* string -> timestamp (text and varchar sources) */
+Datum
+formatcast_str_to_timestamp(PG_FUNCTION_ARGS)
+{
+ return formatcast_in_timestamp(PG_GETARG_TEXT_PP(0), PG_GETARG_TEXT_PP(1),
+ PG_GET_COLLATION());
+}
+
+/* string -> timestamp (bpchar source; trailing blanks trimmed) */
+Datum
+formatcast_bpchar_to_timestamp(PG_FUNCTION_ARGS)
+{
+ return formatcast_in_timestamp(formatcast_rtrim_blanks(PG_GETARG_TEXT_PP(0)),
+ PG_GETARG_TEXT_PP(1), PG_GET_COLLATION());
+}
+
+/* string -> timestamptz (text and varchar sources) */
+Datum
+formatcast_str_to_timestamptz(PG_FUNCTION_ARGS)
+{
+ return formatcast_in_timestamptz(PG_GETARG_TEXT_PP(0), PG_GETARG_TEXT_PP(1),
+ PG_GET_COLLATION());
+}
+
+/* string -> timestamptz (bpchar source; trailing blanks trimmed) */
+Datum
+formatcast_bpchar_to_timestamptz(PG_FUNCTION_ARGS)
+{
+ return formatcast_in_timestamptz(formatcast_rtrim_blanks(PG_GETARG_TEXT_PP(0)),
+ PG_GETARG_TEXT_PP(1), PG_GET_COLLATION());
+}
+
+/*
+ * Call a collation-aware (datetime, text) -> text formatting function,
+ * propagating a SQL NULL result. DirectFunctionCall cannot handle a SQL NULL
+ * result, so build a local FunctionCallInfo. Pass the caller's flinfo to
+ * provide a normal fmgr context; the callees used here do not depend on
+ * function-specific flinfo state.
+ */
+static Datum
+formatcast_to_char_call(FunctionCallInfo caller_fcinfo, PGFunction fn,
+ Oid collid, Datum value, Datum fmt, bool *resultnull)
+{
+ LOCAL_FCINFO(fcinfo, 2);
+ Datum result;
+
+ InitFunctionCallInfoData(*fcinfo, caller_fcinfo->flinfo, 2, collid,
+ NULL, NULL);
+ fcinfo->args[0].value = value;
+ fcinfo->args[0].isnull = false;
+ fcinfo->args[1].value = fmt;
+ fcinfo->args[1].isnull = false;
+
+ result = (*fn) (fcinfo);
+ *resultnull = fcinfo->isnull;
+ return result;
+}
+
+/*
+ * Per-source-type helpers shared by the datetime -> string wrappers below.
+ * Each formats the source value with the given template, returning the result
+ * text (with *resultnull set on an empty template, as for to_char()).
+ */
+static Datum
+formatcast_date_out(FunctionCallInfo fcinfo, bool *resultnull)
+{
+ DateADT dateVal = PG_GETARG_DATEADT(0);
+ text *fmt = PG_GETARG_TEXT_PP(1);
+ Timestamp ts = date2timestamp_safe(dateVal, NULL);
+
+ return formatcast_to_char_call(fcinfo, timestamp_to_char, PG_GET_COLLATION(),
+ TimestampGetDatum(ts), PointerGetDatum(fmt),
+ resultnull);
+}
+
+static Datum
+formatcast_timestamp_out(FunctionCallInfo fcinfo, bool *resultnull)
+{
+ return formatcast_to_char_call(fcinfo, timestamp_to_char, PG_GET_COLLATION(),
+ PG_GETARG_DATUM(0), PG_GETARG_DATUM(1),
+ resultnull);
+}
+
+static Datum
+formatcast_timestamptz_out(FunctionCallInfo fcinfo, bool *resultnull)
+{
+ return formatcast_to_char_call(fcinfo, timestamptz_to_char, PG_GET_COLLATION(),
+ PG_GETARG_DATUM(0), PG_GETARG_DATUM(1),
+ resultnull);
+}
+
+/*
+ * Exported datetime -> string wrappers. The text, varchar and bpchar targets
+ * each get a distinct C symbol so that no two built-in functions share a
+ * prosrc with a different return type; they all delegate to the per-source
+ * helper above, so there is no duplicated logic. The returned text varlena is
+ * a valid varchar/bpchar value; any declared length/typmod is enforced by the
+ * ordinary coercion layered above the CoerceViaFormatter node.
+ */
+Datum
+formatcast_date_to_text(PG_FUNCTION_ARGS)
+{
+ bool resultnull;
+ Datum result = formatcast_date_out(fcinfo, &resultnull);
+
+ if (resultnull)
+ PG_RETURN_NULL();
+ PG_RETURN_DATUM(result);
+}
+
+Datum
+formatcast_date_to_varchar(PG_FUNCTION_ARGS)
+{
+ bool resultnull;
+ Datum result = formatcast_date_out(fcinfo, &resultnull);
+
+ if (resultnull)
+ PG_RETURN_NULL();
+ PG_RETURN_DATUM(result);
+}
+
+Datum
+formatcast_date_to_bpchar(PG_FUNCTION_ARGS)
+{
+ bool resultnull;
+ Datum result = formatcast_date_out(fcinfo, &resultnull);
+
+ if (resultnull)
+ PG_RETURN_NULL();
+ PG_RETURN_DATUM(result);
+}
+
+Datum
+formatcast_timestamp_to_text(PG_FUNCTION_ARGS)
+{
+ bool resultnull;
+ Datum result = formatcast_timestamp_out(fcinfo, &resultnull);
+
+ if (resultnull)
+ PG_RETURN_NULL();
+ PG_RETURN_DATUM(result);
+}
+
+Datum
+formatcast_timestamp_to_varchar(PG_FUNCTION_ARGS)
+{
+ bool resultnull;
+ Datum result = formatcast_timestamp_out(fcinfo, &resultnull);
+
+ if (resultnull)
+ PG_RETURN_NULL();
+ PG_RETURN_DATUM(result);
+}
+
+Datum
+formatcast_timestamp_to_bpchar(PG_FUNCTION_ARGS)
+{
+ bool resultnull;
+ Datum result = formatcast_timestamp_out(fcinfo, &resultnull);
+
+ if (resultnull)
+ PG_RETURN_NULL();
+ PG_RETURN_DATUM(result);
+}
+
+Datum
+formatcast_timestamptz_to_text(PG_FUNCTION_ARGS)
+{
+ bool resultnull;
+ Datum result = formatcast_timestamptz_out(fcinfo, &resultnull);
+
+ if (resultnull)
+ PG_RETURN_NULL();
+ PG_RETURN_DATUM(result);
+}
+
+Datum
+formatcast_timestamptz_to_varchar(PG_FUNCTION_ARGS)
+{
+ bool resultnull;
+ Datum result = formatcast_timestamptz_out(fcinfo, &resultnull);
+
+ if (resultnull)
+ PG_RETURN_NULL();
+ PG_RETURN_DATUM(result);
+}
+
+Datum
+formatcast_timestamptz_to_bpchar(PG_FUNCTION_ARGS)
+{
+ bool resultnull;
+ Datum result = formatcast_timestamptz_out(fcinfo, &resultnull);
+
+ if (resultnull)
+ PG_RETURN_NULL();
+ PG_RETURN_DATUM(result);
+}
+
/*
* Convert the 'date_txt' input to a datetime type using argument 'fmt'
* as a format string. The collation 'collid' may be used for case-folding
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 92883740743..eef3da00cf9 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2188,6 +2188,29 @@ selectDumpableCast(CastInfo *cast, Archive *fout)
DUMP_COMPONENT_ALL : DUMP_COMPONENT_NONE;
}
+/*
+ * selectDumpableFormatter: policy-setting subroutine
+ * Mark a formatter as to be dumped or not
+ *
+ * Like casts, formatters have no namespace and no identifiable owner, so we
+ * distinguish the built-in (initdb-created) formatters from user-defined ones
+ * by checking whether the formatter's OID is in the range reserved for initdb.
+ * Built-in formatters are part of the system catalogs and must not be dumped
+ * as CREATE FORMATTER commands.
+ */
+static void
+selectDumpableFormatter(FormatterInfo *formatter, Archive *fout)
+{
+ if (checkExtensionMembership(&formatter->dobj, fout))
+ return; /* extension membership overrides all else */
+
+ if (formatter->dobj.catId.oid <= g_last_builtin_oid)
+ formatter->dobj.dump = DUMP_COMPONENT_NONE;
+ else
+ formatter->dobj.dump = fout->dopt->include_everything ?
+ DUMP_COMPONENT_ALL : DUMP_COMPONENT_NONE;
+}
+
/*
* selectDumpableProcLang: policy-setting subroutine
* Mark a procedural language as to be dumped or not
@@ -9390,7 +9413,7 @@ getFormatters(Archive *fout)
formatterinfo[i].dobj.name = namebuf.data;
/* Decide whether we want to dump it */
- selectDumpableObject(&(formatterinfo[i].dobj), fout);
+ selectDumpableFormatter(&(formatterinfo[i]), fout);
}
PQclear(res);
diff --git a/src/include/catalog/pg_formatter.dat b/src/include/catalog/pg_formatter.dat
new file mode 100644
index 00000000000..4518071039e
--- /dev/null
+++ b/src/include/catalog/pg_formatter.dat
@@ -0,0 +1,71 @@
+#----------------------------------------------------------------------
+#
+# pg_formatter.dat
+# Initial contents of the pg_formatter system catalog.
+#
+# These are the built-in formatters for CAST ( <operand> AS <type>
+# FORMAT <template> ). Each row maps an exact (source type, target type) pair
+# to a wrapper function with the signature
+# formatter(source_type, text) returns target_type
+# Lookup is by exact source and target type, so text, varchar and bpchar are
+# registered separately on each side. The wrapper functions live in
+# formatting.c and reuse PostgreSQL's existing formatting code.
+#
+# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/include/catalog/pg_formatter.dat
+#
+#----------------------------------------------------------------------
+
+[
+
+# string -> date
+{ oid => '8668', fmtsource => 'text', fmttarget => 'date',
+ fmtfunc => 'pg_format_text_to_date(text,text)' },
+{ oid => '8669', fmtsource => 'varchar', fmttarget => 'date',
+ fmtfunc => 'pg_format_varchar_to_date(varchar,text)' },
+{ oid => '8670', fmtsource => 'bpchar', fmttarget => 'date',
+ fmtfunc => 'pg_format_bpchar_to_date(bpchar,text)' },
+
+# string -> timestamp without time zone
+{ oid => '8671', fmtsource => 'text', fmttarget => 'timestamp',
+ fmtfunc => 'pg_format_text_to_timestamp(text,text)' },
+{ oid => '8672', fmtsource => 'varchar', fmttarget => 'timestamp',
+ fmtfunc => 'pg_format_varchar_to_timestamp(varchar,text)' },
+{ oid => '8673', fmtsource => 'bpchar', fmttarget => 'timestamp',
+ fmtfunc => 'pg_format_bpchar_to_timestamp(bpchar,text)' },
+
+# string -> timestamp with time zone
+{ oid => '8674', fmtsource => 'text', fmttarget => 'timestamptz',
+ fmtfunc => 'pg_format_text_to_timestamptz(text,text)' },
+{ oid => '8675', fmtsource => 'varchar', fmttarget => 'timestamptz',
+ fmtfunc => 'pg_format_varchar_to_timestamptz(varchar,text)' },
+{ oid => '8676', fmtsource => 'bpchar', fmttarget => 'timestamptz',
+ fmtfunc => 'pg_format_bpchar_to_timestamptz(bpchar,text)' },
+
+# date -> string
+{ oid => '8677', fmtsource => 'date', fmttarget => 'text',
+ fmtfunc => 'pg_format_date_to_text(date,text)' },
+{ oid => '8678', fmtsource => 'date', fmttarget => 'varchar',
+ fmtfunc => 'pg_format_date_to_varchar(date,text)' },
+{ oid => '8679', fmtsource => 'date', fmttarget => 'bpchar',
+ fmtfunc => 'pg_format_date_to_bpchar(date,text)' },
+
+# timestamp without time zone -> string
+{ oid => '8680', fmtsource => 'timestamp', fmttarget => 'text',
+ fmtfunc => 'pg_format_timestamp_to_text(timestamp,text)' },
+{ oid => '8681', fmtsource => 'timestamp', fmttarget => 'varchar',
+ fmtfunc => 'pg_format_timestamp_to_varchar(timestamp,text)' },
+{ oid => '8682', fmtsource => 'timestamp', fmttarget => 'bpchar',
+ fmtfunc => 'pg_format_timestamp_to_bpchar(timestamp,text)' },
+
+# timestamp with time zone -> string
+{ oid => '8683', fmtsource => 'timestamptz', fmttarget => 'text',
+ fmtfunc => 'pg_format_timestamptz_to_text(timestamptz,text)' },
+{ oid => '8684', fmtsource => 'timestamptz', fmttarget => 'varchar',
+ fmtfunc => 'pg_format_timestamptz_to_varchar(timestamptz,text)' },
+{ oid => '8685', fmtsource => 'timestamptz', fmttarget => 'bpchar',
+ fmtfunc => 'pg_format_timestamptz_to_bpchar(timestamptz,text)' },
+
+]
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 402d869710b..40d8f33497f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4847,6 +4847,83 @@
{ oid => '1780', descr => 'convert text to date',
proname => 'to_date', provolatile => 's', prorettype => 'date',
proargtypes => 'text text', prosrc => 'to_date' },
+
+# Built-in formatters for CAST ( ... FORMAT ... ). These are thin wrappers
+# around the existing to_char/to_date/to_timestamp/parse_datetime machinery,
+# registered in pg_formatter (see pg_formatter.dat) and reached only through a
+# CoerceViaFormatter node; they are not a primary user-facing API.
+{ oid => '8650', descr => 'formatted cast: text to date',
+ proname => 'pg_format_text_to_date', proisstrict => 't', provolatile => 's',
+ prorettype => 'date', proargtypes => 'text text',
+ prosrc => 'formatcast_str_to_date' },
+{ oid => '8651', descr => 'formatted cast: varchar to date',
+ proname => 'pg_format_varchar_to_date', proisstrict => 't', provolatile => 's',
+ prorettype => 'date', proargtypes => 'varchar text',
+ prosrc => 'formatcast_str_to_date' },
+{ oid => '8652', descr => 'formatted cast: bpchar to date',
+ proname => 'pg_format_bpchar_to_date', proisstrict => 't', provolatile => 's',
+ prorettype => 'date', proargtypes => 'bpchar text',
+ prosrc => 'formatcast_bpchar_to_date' },
+{ oid => '8653', descr => 'formatted cast: text to timestamp',
+ proname => 'pg_format_text_to_timestamp', proisstrict => 't', provolatile => 's',
+ prorettype => 'timestamp', proargtypes => 'text text',
+ prosrc => 'formatcast_str_to_timestamp' },
+{ oid => '8654', descr => 'formatted cast: varchar to timestamp',
+ proname => 'pg_format_varchar_to_timestamp', proisstrict => 't', provolatile => 's',
+ prorettype => 'timestamp', proargtypes => 'varchar text',
+ prosrc => 'formatcast_str_to_timestamp' },
+{ oid => '8655', descr => 'formatted cast: bpchar to timestamp',
+ proname => 'pg_format_bpchar_to_timestamp', proisstrict => 't', provolatile => 's',
+ prorettype => 'timestamp', proargtypes => 'bpchar text',
+ prosrc => 'formatcast_bpchar_to_timestamp' },
+{ oid => '8656', descr => 'formatted cast: text to timestamptz',
+ proname => 'pg_format_text_to_timestamptz', proisstrict => 't', provolatile => 's',
+ prorettype => 'timestamptz', proargtypes => 'text text',
+ prosrc => 'formatcast_str_to_timestamptz' },
+{ oid => '8657', descr => 'formatted cast: varchar to timestamptz',
+ proname => 'pg_format_varchar_to_timestamptz', proisstrict => 't', provolatile => 's',
+ prorettype => 'timestamptz', proargtypes => 'varchar text',
+ prosrc => 'formatcast_str_to_timestamptz' },
+{ oid => '8658', descr => 'formatted cast: bpchar to timestamptz',
+ proname => 'pg_format_bpchar_to_timestamptz', proisstrict => 't', provolatile => 's',
+ prorettype => 'timestamptz', proargtypes => 'bpchar text',
+ prosrc => 'formatcast_bpchar_to_timestamptz' },
+{ oid => '8659', descr => 'formatted cast: date to text',
+ proname => 'pg_format_date_to_text', proisstrict => 't', provolatile => 's',
+ prorettype => 'text', proargtypes => 'date text',
+ prosrc => 'formatcast_date_to_text' },
+{ oid => '8660', descr => 'formatted cast: date to varchar',
+ proname => 'pg_format_date_to_varchar', proisstrict => 't', provolatile => 's',
+ prorettype => 'varchar', proargtypes => 'date text',
+ prosrc => 'formatcast_date_to_varchar' },
+{ oid => '8661', descr => 'formatted cast: date to bpchar',
+ proname => 'pg_format_date_to_bpchar', proisstrict => 't', provolatile => 's',
+ prorettype => 'bpchar', proargtypes => 'date text',
+ prosrc => 'formatcast_date_to_bpchar' },
+{ oid => '8662', descr => 'formatted cast: timestamp to text',
+ proname => 'pg_format_timestamp_to_text', proisstrict => 't', provolatile => 's',
+ prorettype => 'text', proargtypes => 'timestamp text',
+ prosrc => 'formatcast_timestamp_to_text' },
+{ oid => '8663', descr => 'formatted cast: timestamp to varchar',
+ proname => 'pg_format_timestamp_to_varchar', proisstrict => 't', provolatile => 's',
+ prorettype => 'varchar', proargtypes => 'timestamp text',
+ prosrc => 'formatcast_timestamp_to_varchar' },
+{ oid => '8664', descr => 'formatted cast: timestamp to bpchar',
+ proname => 'pg_format_timestamp_to_bpchar', proisstrict => 't', provolatile => 's',
+ prorettype => 'bpchar', proargtypes => 'timestamp text',
+ prosrc => 'formatcast_timestamp_to_bpchar' },
+{ oid => '8665', descr => 'formatted cast: timestamptz to text',
+ proname => 'pg_format_timestamptz_to_text', proisstrict => 't', provolatile => 's',
+ prorettype => 'text', proargtypes => 'timestamptz text',
+ prosrc => 'formatcast_timestamptz_to_text' },
+{ oid => '8666', descr => 'formatted cast: timestamptz to varchar',
+ proname => 'pg_format_timestamptz_to_varchar', proisstrict => 't', provolatile => 's',
+ prorettype => 'varchar', proargtypes => 'timestamptz text',
+ prosrc => 'formatcast_timestamptz_to_varchar' },
+{ oid => '8667', descr => 'formatted cast: timestamptz to bpchar',
+ proname => 'pg_format_timestamptz_to_bpchar', proisstrict => 't', provolatile => 's',
+ prorettype => 'bpchar', proargtypes => 'timestamptz text',
+ prosrc => 'formatcast_timestamptz_to_bpchar' },
{ oid => '1768', descr => 'format interval to text',
proname => 'to_char', provolatile => 's', prorettype => 'text',
proargtypes => 'interval text', prosrc => 'interval_to_char' },
diff --git a/src/test/regress/expected/expressions.out b/src/test/regress/expected/expressions.out
index 7d064024b3a..bc5b37f9cc4 100644
--- a/src/test/regress/expected/expressions.out
+++ b/src/test/regress/expected/expressions.out
@@ -565,21 +565,22 @@ rollback;
-- CAST(expr AS type FORMAT format_expr)
--
-- A FORMAT clause is resolved through a registered formatter (see CREATE
--- FORMATTER) and never falls back to an ordinary cast. No formatters are
--- defined in this test, so these casts fail with a missing-formatter error;
--- this confirms the FORMAT clause is neither ignored nor treated as an
--- ordinary cast. An unknown-type source literal is coerced to text first,
--- so the lookup key is (text, target).
--- basic form (looks up a formatter for (text, date))
-SELECT CAST('2026-06-24' AS date FORMAT 'YYYY-MM-DD');
-ERROR: formatter for cast from type text to type date does not exist
-LINE 1: SELECT CAST('2026-06-24' AS date FORMAT 'YYYY-MM-DD');
+-- FORMATTER) and never falls back to an ordinary cast. The type pairs used
+-- here have no formatter (the built-in formatters cover datetime/string pairs,
+-- not these), so these casts fail with a missing-formatter error even though an
+-- ordinary cast would succeed; this confirms the FORMAT clause is neither
+-- ignored nor treated as an ordinary cast. An unknown-type source literal is
+-- coerced to text first, so the lookup key is (text, target).
+-- basic form (looks up a formatter for (text, integer); none exists)
+SELECT CAST('42' AS integer FORMAT 'whatever');
+ERROR: formatter for cast from type text to type integer does not exist
+LINE 1: SELECT CAST('42' AS integer FORMAT 'whatever');
^
HINT: Use CREATE FORMATTER to define a formatter for this type pair.
-- the format may be a general expression, not just a string literal
-SELECT CAST('2026-06-24' AS date FORMAT 'YYYY' || '-MM-DD');
-ERROR: formatter for cast from type text to type date does not exist
-LINE 1: SELECT CAST('2026-06-24' AS date FORMAT 'YYYY' || '-MM-DD');
+SELECT CAST('42' AS integer FORMAT 'what' || 'ever');
+ERROR: formatter for cast from type text to type integer does not exist
+LINE 1: SELECT CAST('42' AS integer FORMAT 'what' || 'ever');
^
HINT: Use CREATE FORMATTER to define a formatter for this type pair.
-- a no-op-looking cast must not be relabeled away; it needs a (text, text) formatter
diff --git a/src/test/regress/expected/formatters.out b/src/test/regress/expected/formatters.out
index 6a47f007f12..2d2cd5d01fa 100644
--- a/src/test/regress/expected/formatters.out
+++ b/src/test/regress/expected/formatters.out
@@ -59,7 +59,9 @@ CREATE FUNCTION fmt_bad_set(integer, text) RETURNS SETOF text
CREATE FORMATTER FOR CAST (integer AS text)
WITH FUNCTION fmt_bad_set(integer, text); -- set-returning rejected
ERROR: formatter function must not return a set
--- No pseudo-types
+-- No pseudo-types: a formatter's source/target may not be a pseudo-type, so
+-- CREATE FORMATTER on a polymorphic (anyelement) source is intentionally
+-- rejected.
CREATE FUNCTION fmt_anyel(anyelement, text) RETURNS text
LANGUAGE sql IMMUTABLE AS $$ SELECT $2 $$;
CREATE FORMATTER FOR CAST (anyelement AS text)
@@ -357,3 +359,281 @@ SELECT count(*) FROM pg_class WHERE relname = 'formatter_chain_view';
0
(1 row)
+-- ====================================================================
+-- Built-in formatters: SQL-standard-style (SQL:2016/SQL:2023) formatted casts
+-- between character-string and datetime types, registered in pg_formatter and
+-- reached through CoerceViaFormatter (no parser/analyzer hard-coding of
+-- function names, and no fallback to ordinary casts). The built-ins reuse
+-- PostgreSQL's existing to_char/to_date/to_timestamp template engine; they do
+-- not claim byte-for-byte conformance to every SQL-standard template rule.
+-- Included: date, timestamp, timestamptz with text/varchar/bpchar on the other
+-- side. Deferred: time, timetz, numeric, interval, and full standard
+-- template-token coverage.
+-- ====================================================================
+-- Pin the time zone so timestamptz results are stable across machines.
+SET TimeZone = 'UTC';
+-- text -> date (the source is an unknown-type literal coerced to text, so this
+-- also exercises Patch 3's unknown -> text handling).
+SELECT CAST('2026-06-28' AS date FORMAT 'YYYY-MM-DD');
+ date
+------------
+ 2026-06-28
+(1 row)
+
+-- varchar source -> date
+SELECT CAST('2026-06-28'::varchar AS date FORMAT 'YYYY-MM-DD');
+ date
+------------
+ 2026-06-28
+(1 row)
+
+-- bpchar source -> date: a CHAR(10) holds the value exactly, and a wider
+-- CHAR(20) pads with blanks that the formatter trims before parsing.
+SELECT CAST('2026-06-28'::char(10) AS date FORMAT 'YYYY-MM-DD');
+ date
+------------
+ 2026-06-28
+(1 row)
+
+SELECT CAST('2026-06-28'::char(20) AS date FORMAT 'YYYY-MM-DD');
+ date
+------------
+ 2026-06-28
+(1 row)
+
+-- The built-in formatter functions are STRICT: a NULL source or a NULL FORMAT
+-- expression yields NULL without invoking the formatter.
+SELECT CAST(NULL::text AS date FORMAT 'YYYY-MM-DD') IS NULL AS null_src;
+ null_src
+----------
+ t
+(1 row)
+
+SELECT CAST('2026-06-28' AS date FORMAT NULL::text) IS NULL AS null_fmt;
+ null_fmt
+----------
+ t
+(1 row)
+
+SELECT CAST(NULL::date AS text FORMAT 'YYYY-MM-DD') IS NULL AS null_src_out;
+ null_src_out
+--------------
+ t
+(1 row)
+
+-- date -> text / varchar
+SELECT CAST(date '2026-06-28' AS text FORMAT 'YYYY-MM-DD');
+ text
+------------
+ 2026-06-28
+(1 row)
+
+SELECT CAST(date '2026-06-28' AS varchar FORMAT 'YYYY-MM-DD');
+ varchar
+------------
+ 2026-06-28
+(1 row)
+
+-- date -> varchar with a typmod: the declared length is enforced by the
+-- ordinary coercion layered above the formatter (here it truncates to 7).
+SELECT CAST(date '2026-06-28' AS varchar(7) FORMAT 'YYYY-MM-DD');
+ varchar
+---------
+ 2026-06
+(1 row)
+
+-- date -> bpchar: AS char(12) pads to the declared length; bare AS char is
+-- char(1), so the ordinary coercion truncates to one character. The wrapper
+-- returns a text varlena that is a valid bpchar value.
+SELECT CAST(date '2026-06-28' AS char(12) FORMAT 'YYYY-MM-DD');
+ bpchar
+--------------
+ 2026-06-28
+(1 row)
+
+SELECT CAST(date '2026-06-28' AS char FORMAT 'YYYY-MM-DD');
+ bpchar
+--------
+ 2
+(1 row)
+
+-- Collation: a formatted cast is collated like the function call it executes.
+-- A collatable result type (text) must get a real result collation, not
+-- InvalidOid; "collation for" reports "default" (it would be NULL if the
+-- result collation were unset), and an explicit COLLATE on the result works.
+SELECT pg_collation_for(CAST(date '2026-06-28' AS text FORMAT 'YYYY-MM-DD'));
+ pg_collation_for
+------------------
+ "default"
+(1 row)
+
+SELECT pg_collation_for(CAST(date '2026-06-28' AS text FORMAT 'YYYY-MM-DD') COLLATE "C");
+ pg_collation_for
+------------------
+ "C"
+(1 row)
+
+-- An explicit COLLATE on the FORMAT argument flows in as the input collation.
+SELECT pg_collation_for(CAST(date '2026-06-28' AS text
+ FORMAT 'YYYY-MM-DD'::text COLLATE "C"));
+ pg_collation_for
+------------------
+ "C"
+(1 row)
+
+-- timestamp without time zone, both directions
+SELECT CAST('2026-06-28 13:45:30' AS timestamp FORMAT 'YYYY-MM-DD HH24:MI:SS');
+ timestamp
+---------------------
+ 2026-06-28 13:45:30
+(1 row)
+
+SELECT CAST(timestamp '2026-06-28 13:45:30' AS text FORMAT 'YYYY-MM-DD HH24:MI:SS');
+ text
+---------------------
+ 2026-06-28 13:45:30
+(1 row)
+
+-- a date-only template is widened to timestamp (midnight)
+SELECT CAST('2026-06-28' AS timestamp FORMAT 'YYYY-MM-DD');
+ timestamp
+---------------------
+ 2026-06-28 00:00:00
+(1 row)
+
+-- The built-ins reuse PostgreSQL's existing formatting parser: a template with
+-- no time-zone field consumes only the fields it names, so a trailing time zone
+-- in the input is simply ignored (existing to_timestamp() behavior, unchanged).
+SELECT CAST('2026-06-28 13:45:30+05' AS timestamp FORMAT 'YYYY-MM-DD HH24:MI:SS');
+ timestamp
+---------------------
+ 2026-06-28 13:45:30
+(1 row)
+
+-- But a template that explicitly contains a time-zone field is rejected for a
+-- timestamp-without-time-zone target (we must not fold a zone into a zoneless
+-- result via the session TimeZone).
+SELECT CAST('2026-06-28 13:45:30+05' AS timestamp FORMAT 'YYYY-MM-DD HH24:MI:SS TZH');
+ERROR: FORMAT template for a cast to timestamp without time zone must not specify a time zone
+-- timestamp WITHOUT time zone must not depend on the session TimeZone: the same
+-- (unzoned) input/template yields the same wall-clock value under any zone.
+SET TimeZone = 'America/Los_Angeles';
+SELECT CAST('2026-06-28 13:45:30' AS timestamp FORMAT 'YYYY-MM-DD HH24:MI:SS');
+ timestamp
+---------------------
+ 2026-06-28 13:45:30
+(1 row)
+
+SET TimeZone = 'UTC';
+SELECT CAST('2026-06-28 13:45:30' AS timestamp FORMAT 'YYYY-MM-DD HH24:MI:SS');
+ timestamp
+---------------------
+ 2026-06-28 13:45:30
+(1 row)
+
+-- timestamp with time zone, both directions (TimeZone is UTC, set above)
+SELECT CAST('2026-06-28 13:45:30 +00' AS timestamptz FORMAT 'YYYY-MM-DD HH24:MI:SS TZH');
+ timestamptz
+------------------------
+ 2026-06-28 13:45:30+00
+(1 row)
+
+SELECT CAST(timestamptz '2026-06-28 13:45:30+00' AS text FORMAT 'YYYY-MM-DD HH24:MI:SS TZH');
+ text
+-------------------------
+ 2026-06-28 13:45:30 +00
+(1 row)
+
+-- A sampling of SQL-standard-style datetime template tokens, exercised through
+-- the existing to_char/to_timestamp engine. TZM and SSSSS are supported here;
+-- this is not exhaustive standard-token coverage.
+SELECT CAST(timestamptz '2026-06-28 13:45:30+00' AS text
+ FORMAT 'YYYY-MM-DD"T"HH24:MI:SS TZH:TZM');
+ text
+----------------------------
+ 2026-06-28T13:45:30 +00:00
+(1 row)
+
+SELECT CAST(timestamp '2026-06-28 13:45:30' AS text FORMAT 'SSSSS');
+ text
+-------
+ 49530
+(1 row)
+
+-- Fractional seconds (FFn) are accepted by to_char in the output direction.
+SELECT CAST(timestamp '2026-06-28 13:45:30.123456' AS text
+ FORMAT 'HH24:MI:SS.FF6');
+ text
+-----------------
+ 13:45:30.123456
+(1 row)
+
+-- Empty FORMAT template: stable, never a crash. In the parse direction the
+-- underlying to_date() engine parses no fields and returns the all-defaults
+-- date; in the format direction the wrappers mirror to_char(value, ''), which
+-- is NULL.
+SELECT CAST('2026-06-28' AS date FORMAT '');
+ date
+---------------
+ 0001-01-01 BC
+(1 row)
+
+SELECT CAST(date '2026-06-28' AS text FORMAT '') IS NULL AS empty_fmt_out;
+ empty_fmt_out
+---------------
+ t
+(1 row)
+
+-- The FORMAT clause remains a PostgreSQL extension: it may be any expression
+-- coercible to text, not just a string literal.
+SELECT CAST('2026-' || '06-28' AS date FORMAT 'YYYY-' || 'MM-DD');
+ date
+------------
+ 2026-06-28
+(1 row)
+
+-- A FORMAT clause never falls back to an ordinary cast: an integer -> date
+-- pair has no formatter (and no ordinary cast), so it errors.
+SELECT CAST(5 AS date FORMAT 'YYYY');
+ERROR: formatter for cast from type integer to type date does not exist
+LINE 1: SELECT CAST(5 AS date FORMAT 'YYYY');
+ ^
+HINT: Use CREATE FORMATTER to define a formatter for this type pair.
+-- A built-in formatter occupies its (source, target) pair, so a user
+-- CREATE FORMATTER for the same pair fails with the usual duplicate error.
+CREATE FUNCTION my_text_to_date(text, text) RETURNS date
+ LANGUAGE sql IMMUTABLE RETURN to_date($1, $2);
+CREATE FORMATTER FOR CAST (text AS date)
+ WITH FUNCTION my_text_to_date(text, text); -- fails: already exists
+ERROR: formatter for cast from type text to type date already exists
+DROP FUNCTION my_text_to_date(text, text);
+-- Built-in formatter rows are system objects (their OIDs are in the pinned
+-- range), so they cannot be dropped.
+DROP FORMATTER FOR CAST (text AS date); -- fails: pinned system object
+ERROR: cannot drop formatter for cast from text to date because it is required by the database system
+-- A view over a built-in formatted cast deparses back to CAST(... FORMAT ...).
+CREATE VIEW builtin_formatter_view AS
+ SELECT CAST('2026-06-28' AS date FORMAT 'YYYY-MM-DD') AS d;
+SELECT pg_get_viewdef('builtin_formatter_view'::regclass, true);
+ pg_get_viewdef
+--------------------------------------------------------------------------
+ SELECT CAST('2026-06-28'::text AS date FORMAT 'YYYY-MM-DD'::text) AS d;
+(1 row)
+
+SELECT * FROM builtin_formatter_view;
+ d
+------------
+ 2026-06-28
+(1 row)
+
+DROP VIEW builtin_formatter_view;
+-- Patch 3 added a new expression node; make sure query jumbling handles it.
+SET compute_query_id = on;
+SELECT CAST('2026-06-28' AS date FORMAT 'YYYY-MM-DD');
+ date
+------------
+ 2026-06-28
+(1 row)
+
+RESET compute_query_id;
+RESET TimeZone;
diff --git a/src/test/regress/sql/expressions.sql b/src/test/regress/sql/expressions.sql
index 46575bc410d..67aef429398 100644
--- a/src/test/regress/sql/expressions.sql
+++ b/src/test/regress/sql/expressions.sql
@@ -306,17 +306,18 @@ rollback;
-- CAST(expr AS type FORMAT format_expr)
--
-- A FORMAT clause is resolved through a registered formatter (see CREATE
--- FORMATTER) and never falls back to an ordinary cast. No formatters are
--- defined in this test, so these casts fail with a missing-formatter error;
--- this confirms the FORMAT clause is neither ignored nor treated as an
--- ordinary cast. An unknown-type source literal is coerced to text first,
--- so the lookup key is (text, target).
+-- FORMATTER) and never falls back to an ordinary cast. The type pairs used
+-- here have no formatter (the built-in formatters cover datetime/string pairs,
+-- not these), so these casts fail with a missing-formatter error even though an
+-- ordinary cast would succeed; this confirms the FORMAT clause is neither
+-- ignored nor treated as an ordinary cast. An unknown-type source literal is
+-- coerced to text first, so the lookup key is (text, target).
--- basic form (looks up a formatter for (text, date))
-SELECT CAST('2026-06-24' AS date FORMAT 'YYYY-MM-DD');
+-- basic form (looks up a formatter for (text, integer); none exists)
+SELECT CAST('42' AS integer FORMAT 'whatever');
-- the format may be a general expression, not just a string literal
-SELECT CAST('2026-06-24' AS date FORMAT 'YYYY' || '-MM-DD');
+SELECT CAST('42' AS integer FORMAT 'what' || 'ever');
-- a no-op-looking cast must not be relabeled away; it needs a (text, text) formatter
SELECT CAST('abc'::text AS text FORMAT 'whatever');
diff --git a/src/test/regress/sql/formatters.sql b/src/test/regress/sql/formatters.sql
index 2c9b4351652..9ad8e92c42b 100644
--- a/src/test/regress/sql/formatters.sql
+++ b/src/test/regress/sql/formatters.sql
@@ -54,7 +54,9 @@ CREATE FUNCTION fmt_bad_set(integer, text) RETURNS SETOF text
CREATE FORMATTER FOR CAST (integer AS text)
WITH FUNCTION fmt_bad_set(integer, text); -- set-returning rejected
--- No pseudo-types
+-- No pseudo-types: a formatter's source/target may not be a pseudo-type, so
+-- CREATE FORMATTER on a polymorphic (anyelement) source is intentionally
+-- rejected.
CREATE FUNCTION fmt_anyel(anyelement, text) RETURNS text
LANGUAGE sql IMMUTABLE AS $$ SELECT $2 $$;
CREATE FORMATTER FOR CAST (anyelement AS text)
@@ -236,3 +238,130 @@ DROP FUNCTION i2t_chain(integer, text) CASCADE; -- drops formatter and view
SELECT count(*) FROM pg_formatter
WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype;
SELECT count(*) FROM pg_class WHERE relname = 'formatter_chain_view';
+
+-- ====================================================================
+-- Built-in formatters: SQL-standard-style (SQL:2016/SQL:2023) formatted casts
+-- between character-string and datetime types, registered in pg_formatter and
+-- reached through CoerceViaFormatter (no parser/analyzer hard-coding of
+-- function names, and no fallback to ordinary casts). The built-ins reuse
+-- PostgreSQL's existing to_char/to_date/to_timestamp template engine; they do
+-- not claim byte-for-byte conformance to every SQL-standard template rule.
+-- Included: date, timestamp, timestamptz with text/varchar/bpchar on the other
+-- side. Deferred: time, timetz, numeric, interval, and full standard
+-- template-token coverage.
+-- ====================================================================
+-- Pin the time zone so timestamptz results are stable across machines.
+SET TimeZone = 'UTC';
+
+-- text -> date (the source is an unknown-type literal coerced to text, so this
+-- also exercises Patch 3's unknown -> text handling).
+SELECT CAST('2026-06-28' AS date FORMAT 'YYYY-MM-DD');
+-- varchar source -> date
+SELECT CAST('2026-06-28'::varchar AS date FORMAT 'YYYY-MM-DD');
+-- bpchar source -> date: a CHAR(10) holds the value exactly, and a wider
+-- CHAR(20) pads with blanks that the formatter trims before parsing.
+SELECT CAST('2026-06-28'::char(10) AS date FORMAT 'YYYY-MM-DD');
+SELECT CAST('2026-06-28'::char(20) AS date FORMAT 'YYYY-MM-DD');
+
+-- The built-in formatter functions are STRICT: a NULL source or a NULL FORMAT
+-- expression yields NULL without invoking the formatter.
+SELECT CAST(NULL::text AS date FORMAT 'YYYY-MM-DD') IS NULL AS null_src;
+SELECT CAST('2026-06-28' AS date FORMAT NULL::text) IS NULL AS null_fmt;
+SELECT CAST(NULL::date AS text FORMAT 'YYYY-MM-DD') IS NULL AS null_src_out;
+
+-- date -> text / varchar
+SELECT CAST(date '2026-06-28' AS text FORMAT 'YYYY-MM-DD');
+SELECT CAST(date '2026-06-28' AS varchar FORMAT 'YYYY-MM-DD');
+-- date -> varchar with a typmod: the declared length is enforced by the
+-- ordinary coercion layered above the formatter (here it truncates to 7).
+SELECT CAST(date '2026-06-28' AS varchar(7) FORMAT 'YYYY-MM-DD');
+-- date -> bpchar: AS char(12) pads to the declared length; bare AS char is
+-- char(1), so the ordinary coercion truncates to one character. The wrapper
+-- returns a text varlena that is a valid bpchar value.
+SELECT CAST(date '2026-06-28' AS char(12) FORMAT 'YYYY-MM-DD');
+SELECT CAST(date '2026-06-28' AS char FORMAT 'YYYY-MM-DD');
+
+-- Collation: a formatted cast is collated like the function call it executes.
+-- A collatable result type (text) must get a real result collation, not
+-- InvalidOid; "collation for" reports "default" (it would be NULL if the
+-- result collation were unset), and an explicit COLLATE on the result works.
+SELECT pg_collation_for(CAST(date '2026-06-28' AS text FORMAT 'YYYY-MM-DD'));
+SELECT pg_collation_for(CAST(date '2026-06-28' AS text FORMAT 'YYYY-MM-DD') COLLATE "C");
+-- An explicit COLLATE on the FORMAT argument flows in as the input collation.
+SELECT pg_collation_for(CAST(date '2026-06-28' AS text
+ FORMAT 'YYYY-MM-DD'::text COLLATE "C"));
+
+-- timestamp without time zone, both directions
+SELECT CAST('2026-06-28 13:45:30' AS timestamp FORMAT 'YYYY-MM-DD HH24:MI:SS');
+SELECT CAST(timestamp '2026-06-28 13:45:30' AS text FORMAT 'YYYY-MM-DD HH24:MI:SS');
+-- a date-only template is widened to timestamp (midnight)
+SELECT CAST('2026-06-28' AS timestamp FORMAT 'YYYY-MM-DD');
+-- The built-ins reuse PostgreSQL's existing formatting parser: a template with
+-- no time-zone field consumes only the fields it names, so a trailing time zone
+-- in the input is simply ignored (existing to_timestamp() behavior, unchanged).
+SELECT CAST('2026-06-28 13:45:30+05' AS timestamp FORMAT 'YYYY-MM-DD HH24:MI:SS');
+-- But a template that explicitly contains a time-zone field is rejected for a
+-- timestamp-without-time-zone target (we must not fold a zone into a zoneless
+-- result via the session TimeZone).
+SELECT CAST('2026-06-28 13:45:30+05' AS timestamp FORMAT 'YYYY-MM-DD HH24:MI:SS TZH');
+-- timestamp WITHOUT time zone must not depend on the session TimeZone: the same
+-- (unzoned) input/template yields the same wall-clock value under any zone.
+SET TimeZone = 'America/Los_Angeles';
+SELECT CAST('2026-06-28 13:45:30' AS timestamp FORMAT 'YYYY-MM-DD HH24:MI:SS');
+SET TimeZone = 'UTC';
+SELECT CAST('2026-06-28 13:45:30' AS timestamp FORMAT 'YYYY-MM-DD HH24:MI:SS');
+
+-- timestamp with time zone, both directions (TimeZone is UTC, set above)
+SELECT CAST('2026-06-28 13:45:30 +00' AS timestamptz FORMAT 'YYYY-MM-DD HH24:MI:SS TZH');
+SELECT CAST(timestamptz '2026-06-28 13:45:30+00' AS text FORMAT 'YYYY-MM-DD HH24:MI:SS TZH');
+
+-- A sampling of SQL-standard-style datetime template tokens, exercised through
+-- the existing to_char/to_timestamp engine. TZM and SSSSS are supported here;
+-- this is not exhaustive standard-token coverage.
+SELECT CAST(timestamptz '2026-06-28 13:45:30+00' AS text
+ FORMAT 'YYYY-MM-DD"T"HH24:MI:SS TZH:TZM');
+SELECT CAST(timestamp '2026-06-28 13:45:30' AS text FORMAT 'SSSSS');
+-- Fractional seconds (FFn) are accepted by to_char in the output direction.
+SELECT CAST(timestamp '2026-06-28 13:45:30.123456' AS text
+ FORMAT 'HH24:MI:SS.FF6');
+
+-- Empty FORMAT template: stable, never a crash. In the parse direction the
+-- underlying to_date() engine parses no fields and returns the all-defaults
+-- date; in the format direction the wrappers mirror to_char(value, ''), which
+-- is NULL.
+SELECT CAST('2026-06-28' AS date FORMAT '');
+SELECT CAST(date '2026-06-28' AS text FORMAT '') IS NULL AS empty_fmt_out;
+
+-- The FORMAT clause remains a PostgreSQL extension: it may be any expression
+-- coercible to text, not just a string literal.
+SELECT CAST('2026-' || '06-28' AS date FORMAT 'YYYY-' || 'MM-DD');
+
+-- A FORMAT clause never falls back to an ordinary cast: an integer -> date
+-- pair has no formatter (and no ordinary cast), so it errors.
+SELECT CAST(5 AS date FORMAT 'YYYY');
+
+-- A built-in formatter occupies its (source, target) pair, so a user
+-- CREATE FORMATTER for the same pair fails with the usual duplicate error.
+CREATE FUNCTION my_text_to_date(text, text) RETURNS date
+ LANGUAGE sql IMMUTABLE RETURN to_date($1, $2);
+CREATE FORMATTER FOR CAST (text AS date)
+ WITH FUNCTION my_text_to_date(text, text); -- fails: already exists
+DROP FUNCTION my_text_to_date(text, text);
+
+-- Built-in formatter rows are system objects (their OIDs are in the pinned
+-- range), so they cannot be dropped.
+DROP FORMATTER FOR CAST (text AS date); -- fails: pinned system object
+
+-- A view over a built-in formatted cast deparses back to CAST(... FORMAT ...).
+CREATE VIEW builtin_formatter_view AS
+ SELECT CAST('2026-06-28' AS date FORMAT 'YYYY-MM-DD') AS d;
+SELECT pg_get_viewdef('builtin_formatter_view'::regclass, true);
+SELECT * FROM builtin_formatter_view;
+DROP VIEW builtin_formatter_view;
+
+-- Patch 3 added a new expression node; make sure query jumbling handles it.
+SET compute_query_id = on;
+SELECT CAST('2026-06-28' AS date FORMAT 'YYYY-MM-DD');
+RESET compute_query_id;
+
+RESET TimeZone;
--
2.54.0
[application/octet-stream] 0003-Use-CoerceViaFormatter-for-CAST-FORMAT.patch (44.4K, ../../CABXr29FyPC7terFF7E+r462BEHhYgv06oUVoBrhkH7xhshuE6A@mail.gmail.com/3-0003-Use-CoerceViaFormatter-for-CAST-FORMAT.patch)
download | inline diff:
From de709c69237e3304b7369c897842dc362d73ee4f Mon Sep 17 00:00:00 2001
From: Haibo Yan <[email protected]>
Date: Wed, 24 Jun 2026 20:46:00 -0700
Subject: [PATCH] Resolve CAST ... FORMAT with CoerceViaFormatter
Resolve formatted casts through pg_formatter during parse analysis. The
FORMAT expression is transformed and coerced to text, unknown source
literals are treated as text, and formatter lookup is performed by exact
source and target type. If no formatter exists, the cast fails; a FORMAT
clause never falls back to ordinary cast resolution.
Represent the analyzed expression with a CoerceViaFormatter node. The
node preserves CAST ... FORMAT syntax for ruleutils and pg_dump, and
records a dependency on the pg_formatter row as well as on the formatter
function. Execution still uses the ordinary function-call machinery via
ExecInitFunc, so no new executor opcode or JIT support is needed.
Target typmods and domain checks are enforced by the existing coercion
machinery layered above the node.
---
src/backend/catalog/dependency.c | 23 ++
src/backend/executor/execExpr.c | 18 ++
src/backend/nodes/nodeFuncs.c | 53 +++++
src/backend/parser/parse_expr.c | 136 ++++++++++-
src/backend/utils/adt/ruleutils.c | 19 ++
src/backend/utils/cache/lsyscache.c | 37 +++
src/include/nodes/primnodes.h | 38 ++++
src/include/utils/lsyscache.h | 2 +
src/test/regress/expected/expressions.out | 30 +--
src/test/regress/expected/formatters.out | 263 +++++++++++++++++++++-
src/test/regress/sql/expressions.sql | 14 +-
src/test/regress/sql/formatters.sql | 153 ++++++++++++-
12 files changed, 742 insertions(+), 44 deletions(-)
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 7eeac91264f..61e9a28361d 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -2131,6 +2131,29 @@ find_expr_references_walker(Node *node,
add_object_address(CollationRelationId, iocoerce->resultcollid, 0,
context->addrs);
}
+ else if (IsA(node, CoerceViaFormatter))
+ {
+ CoerceViaFormatter *fmt = (CoerceViaFormatter *) node;
+
+ /* depend on the result type */
+ add_object_address(TypeRelationId, fmt->resulttype, 0,
+ context->addrs);
+ /* depend on the formatter function */
+ add_object_address(ProcedureRelationId, fmt->formatterfunc, 0,
+ context->addrs);
+ /*
+ * Also depend on the pg_formatter row itself, so that DROP FORMATTER
+ * is refused (or cascades) while a stored expression uses it.
+ */
+ if (OidIsValid(fmt->formatterid))
+ add_object_address(FormatterRelationId, fmt->formatterid, 0,
+ context->addrs);
+ /* the collation might not be referenced anywhere else, either */
+ if (OidIsValid(fmt->resultcollid) &&
+ fmt->resultcollid != DEFAULT_COLLATION_OID)
+ add_object_address(CollationRelationId, fmt->resultcollid, 0,
+ context->addrs);
+ }
else if (IsA(node, ArrayCoerceExpr))
{
ArrayCoerceExpr *acoerce = (ArrayCoerceExpr *) node;
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index cfea7e160c2..00ef3a8708f 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1200,6 +1200,24 @@ ExecInitExprRec(Expr *node, ExprState *state,
break;
}
+ case T_CoerceViaFormatter:
+ {
+ CoerceViaFormatter *fmt = (CoerceViaFormatter *) node;
+
+ /*
+ * A formatted cast is executed exactly like a call to its
+ * formatter function: formatterfunc(arg, format). Reuse the
+ * ordinary function-call setup, which also performs the
+ * run-time EXECUTE permission check on the formatter function.
+ */
+ ExecInitFunc(&scratch, node,
+ list_make2(fmt->arg, fmt->format),
+ fmt->formatterfunc, fmt->inputcollid,
+ state);
+ ExprEvalPushStep(state, &scratch);
+ break;
+ }
+
case T_OpExpr:
{
OpExpr *op = (OpExpr *) node;
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 66495546179..eac3fee0404 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -182,6 +182,9 @@ exprType(const Node *expr)
case T_CoerceViaIO:
type = ((const CoerceViaIO *) expr)->resulttype;
break;
+ case T_CoerceViaFormatter:
+ type = ((const CoerceViaFormatter *) expr)->resulttype;
+ break;
case T_ArrayCoerceExpr:
type = ((const ArrayCoerceExpr *) expr)->resulttype;
break;
@@ -531,6 +534,8 @@ exprTypmod(const Node *expr)
break;
case T_CoerceToDomain:
return ((const CoerceToDomain *) expr)->resulttypmod;
+ case T_CoerceViaFormatter:
+ return ((const CoerceViaFormatter *) expr)->resulttypmod;
case T_CoerceToDomainValue:
return ((const CoerceToDomainValue *) expr)->typeMod;
case T_SetToDefault:
@@ -943,6 +948,9 @@ exprCollation(const Node *expr)
case T_CoerceViaIO:
coll = ((const CoerceViaIO *) expr)->resultcollid;
break;
+ case T_CoerceViaFormatter:
+ coll = ((const CoerceViaFormatter *) expr)->resultcollid;
+ break;
case T_ArrayCoerceExpr:
coll = ((const ArrayCoerceExpr *) expr)->resultcollid;
break;
@@ -1227,6 +1235,9 @@ exprSetCollation(Node *expr, Oid collation)
case T_CoerceViaIO:
((CoerceViaIO *) expr)->resultcollid = collation;
break;
+ case T_CoerceViaFormatter:
+ ((CoerceViaFormatter *) expr)->resultcollid = collation;
+ break;
case T_ArrayCoerceExpr:
((ArrayCoerceExpr *) expr)->resultcollid = collation;
break;
@@ -1349,6 +1360,9 @@ exprSetInputCollation(Node *expr, Oid inputcollation)
case T_FuncExpr:
((FuncExpr *) expr)->inputcollid = inputcollation;
break;
+ case T_CoerceViaFormatter:
+ ((CoerceViaFormatter *) expr)->inputcollid = inputcollation;
+ break;
case T_OpExpr:
((OpExpr *) expr)->inputcollid = inputcollation;
break;
@@ -1527,6 +1541,15 @@ exprLocation(const Node *expr)
exprLocation((Node *) cexpr->arg));
}
break;
+ case T_CoerceViaFormatter:
+ {
+ const CoerceViaFormatter *cexpr = (const CoerceViaFormatter *) expr;
+
+ /* Much as above */
+ loc = leftmostLoc(cexpr->location,
+ exprLocation((Node *) cexpr->arg));
+ }
+ break;
case T_ArrayCoerceExpr:
{
const ArrayCoerceExpr *cexpr = (const ArrayCoerceExpr *) expr;
@@ -1994,6 +2017,15 @@ check_functions_in_node(Node *node, check_function_callback checker,
return true;
}
break;
+ case T_CoerceViaFormatter:
+ {
+ CoerceViaFormatter *expr = (CoerceViaFormatter *) node;
+
+ /* check the formatter function */
+ if (checker(expr->formatterfunc, context))
+ return true;
+ }
+ break;
case T_RowCompareExpr:
{
RowCompareExpr *rcexpr = (RowCompareExpr *) node;
@@ -2296,6 +2328,16 @@ expression_tree_walker_impl(Node *node,
return WALK(((RelabelType *) node)->arg);
case T_CoerceViaIO:
return WALK(((CoerceViaIO *) node)->arg);
+ case T_CoerceViaFormatter:
+ {
+ CoerceViaFormatter *fmt = (CoerceViaFormatter *) node;
+
+ if (WALK(fmt->arg))
+ return true;
+ if (WALK(fmt->format))
+ return true;
+ }
+ break;
case T_ArrayCoerceExpr:
{
ArrayCoerceExpr *acoerce = (ArrayCoerceExpr *) node;
@@ -3318,6 +3360,17 @@ expression_tree_mutator_impl(Node *node,
return (Node *) newnode;
}
break;
+ case T_CoerceViaFormatter:
+ {
+ CoerceViaFormatter *fmtcoerce = (CoerceViaFormatter *) node;
+ CoerceViaFormatter *newnode;
+
+ FLATCOPY(newnode, fmtcoerce, CoerceViaFormatter);
+ MUTATE(newnode->arg, fmtcoerce->arg, Expr *);
+ MUTATE(newnode->format, fmtcoerce->format, Expr *);
+ return (Node *) newnode;
+ }
+ break;
case T_ArrayCoerceExpr:
{
ArrayCoerceExpr *acoerce = (ArrayCoerceExpr *) node;
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 1729ba56013..243c9121700 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -78,6 +78,7 @@ static Node *transformWholeRowRef(ParseState *pstate,
int sublevels_up, int location);
static Node *transformIndirection(ParseState *pstate, A_Indirection *ind);
static Node *transformTypeCast(ParseState *pstate, TypeCast *tc);
+static Node *transformFormattedTypeCast(ParseState *pstate, TypeCast *tc);
static Node *transformCollateClause(ParseState *pstate, CollateClause *c);
static Node *transformJsonObjectConstructor(ParseState *pstate,
JsonObjectConstructor *ctor);
@@ -2744,17 +2745,12 @@ transformTypeCast(ParseState *pstate, TypeCast *tc)
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.
+ * A FORMAT clause turns this into a formatted cast, which is resolved
+ * exclusively through the pg_formatter catalog (never through ordinary
+ * cast rules). Handle it in a separate code path.
*/
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))));
+ return transformFormattedTypeCast(pstate, tc);
/* Look up the type name first */
typenameTypeIdAndMod(pstate, tc->typeName, &targetType, &targetTypmod);
@@ -2824,6 +2820,128 @@ transformTypeCast(ParseState *pstate, TypeCast *tc)
return result;
}
+/*
+ * Handle a formatted cast: CAST(arg AS typeName FORMAT format).
+ *
+ * Resolved exclusively through the pg_formatter catalog, keyed by
+ * (source type, target type). We build a CoerceViaFormatter node, which at
+ * execution time calls the registered formatter function with the source
+ * value and the FORMAT expression (coerced to text). Using a dedicated node
+ * (rather than a bare FuncExpr) lets the expression deparse back to
+ * CAST ... FORMAT and depend on the pg_formatter row.
+ */
+static Node *
+transformFormattedTypeCast(ParseState *pstate, TypeCast *tc)
+{
+ Node *expr;
+ Node *fmt;
+ Oid sourceType;
+ Oid targetType;
+ int32 targetTypmod;
+ Oid formatterid;
+ Oid fmtfuncid = InvalidOid;
+ Oid funcrettype;
+ CoerceViaFormatter *cvf;
+ Node *result;
+ int location;
+
+ /* Resolve the declared target type, exactly as an ordinary cast does. */
+ typenameTypeIdAndMod(pstate, tc->typeName, &targetType, &targetTypmod);
+
+ /* Transform the source expression. */
+ expr = transformExprRecurse(pstate, tc->arg);
+ sourceType = exprType(expr);
+
+ /*
+ * An unknown-type source (typically a bare string literal or untyped
+ * NULL) is coerced to text before the formatter lookup, so that, e.g.,
+ * CAST('2026-06-24' AS date FORMAT 'YYYY-MM-DD') uses a formatter
+ * registered for (text, date) rather than requiring one for (unknown,
+ * date).
+ */
+ if (sourceType == UNKNOWNOID)
+ {
+ expr = coerce_to_specific_type(pstate, expr, TEXTOID, "CAST");
+ sourceType = exprType(expr);
+ }
+
+ location = tc->location;
+ if (location < 0)
+ location = tc->typeName->location;
+
+ /*
+ * Transform the FORMAT expression and coerce it to text before the
+ * formatter lookup, so an invalid FORMAT expression reports a normal
+ * error rather than being masked by a missing-formatter error.
+ */
+ fmt = transformExprRecurse(pstate, tc->format);
+ fmt = coerce_to_specific_type(pstate, fmt, TEXTOID, "FORMAT");
+
+ /*
+ * Look up the formatter for this (source, target) pair. A FORMAT clause
+ * never falls back to ordinary cast resolution.
+ */
+ formatterid = get_formatter_for_cast(sourceType, targetType, &fmtfuncid);
+ if (!OidIsValid(formatterid))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("formatter for cast from type %s to type %s does not exist",
+ format_type_be(sourceType),
+ format_type_be(targetType)),
+ errhint("Use CREATE FORMATTER to define a formatter for this type pair."),
+ parser_errposition(pstate, location)));
+
+ /*
+ * Build the CoerceViaFormatter node. Its result type is the formatter
+ * function's return type, which CREATE FORMATTER guarantees equals the
+ * target type.
+ *
+ * The collation fields are left unset here; they are assigned later by
+ * parse_collate.c, where CoerceViaFormatter takes the same general n-ary
+ * expression path as FuncExpr (its result/input collation get/set hooks
+ * live in nodeFuncs.c).
+ */
+ funcrettype = get_func_rettype(fmtfuncid);
+ if (!OidIsValid(funcrettype))
+ elog(ERROR, "cache lookup failed for function %u", fmtfuncid);
+ Assert(funcrettype == targetType);
+
+ cvf = makeNode(CoerceViaFormatter);
+ cvf->arg = (Expr *) expr;
+ cvf->format = (Expr *) fmt;
+ cvf->resulttype = funcrettype;
+ cvf->resulttypmod = -1; /* typmod enforced below, if any */
+ cvf->resultcollid = InvalidOid;
+ cvf->inputcollid = InvalidOid;
+ cvf->formatterfunc = fmtfuncid;
+ cvf->formatterid = formatterid;
+ cvf->coercionformat = COERCE_EXPLICIT_CAST;
+ cvf->location = location;
+ result = (Node *) cvf;
+
+ /*
+ * Enforce the declared target type modifier (and any domain constraints)
+ * using the ordinary coercion machinery. In the common case where the
+ * target has no typmod and is not a domain this is a no-op; otherwise it
+ * layers the same length coercion / domain check that an ordinary cast to
+ * the same target would apply.
+ */
+ result = coerce_to_target_type(pstate, result, funcrettype,
+ targetType, targetTypmod,
+ COERCION_EXPLICIT,
+ COERCE_EXPLICIT_CAST,
+ location);
+ if (result == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_CANNOT_COERCE),
+ errmsg("cannot cast type %s to %s",
+ format_type_be(funcrettype),
+ format_type_be(targetType)),
+ parser_errposition(pstate, location)));
+
+ return result;
+}
+
/*
* Handle an explicit COLLATE clause.
*
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 88de5c0481c..76f07c85a8f 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9447,6 +9447,10 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags)
/* function-like: name(..) or name[..] */
return true;
+ case T_CoerceViaFormatter:
+ /* deparses as CAST(.. FORMAT ..), self-delimiting */
+ return true;
+
/* CASE keywords act as parentheses */
case T_CaseExpr:
return true;
@@ -10310,6 +10314,21 @@ get_rule_expr(Node *node, deparse_context *context,
}
break;
+ case T_CoerceViaFormatter:
+ {
+ CoerceViaFormatter *fmt = (CoerceViaFormatter *) node;
+
+ /* always print the SQL-standard CAST(... FORMAT ...) syntax */
+ appendStringInfoString(buf, "CAST(");
+ get_rule_expr((Node *) fmt->arg, context, false);
+ appendStringInfo(buf, " AS %s FORMAT ",
+ format_type_with_typemod(fmt->resulttype,
+ fmt->resulttypmod));
+ get_rule_expr((Node *) fmt->format, context, false);
+ appendStringInfoChar(buf, ')');
+ }
+ break;
+
case T_ArrayCoerceExpr:
{
ArrayCoerceExpr *acoerce = (ArrayCoerceExpr *) node;
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 036de5f79ef..a58111b431f 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -27,6 +27,7 @@
#include "catalog/pg_collation.h"
#include "catalog/pg_constraint.h"
#include "catalog/pg_database.h"
+#include "catalog/pg_formatter.h"
#include "catalog/pg_index.h"
#include "catalog/pg_language.h"
#include "catalog/pg_namespace.h"
@@ -1238,6 +1239,42 @@ get_cast_oid(Oid sourcetypeid, Oid targettypeid, bool missing_ok)
return oid;
}
+/* ---------- PG_FORMATTER CACHE ---------- */
+
+/*
+ * get_formatter_for_cast
+ *
+ * Given source and target type OIDs, look up the formatter registered
+ * for that (source, target) pair. Returns the pg_formatter row OID, or
+ * InvalidOid if none is registered. If found and formatterfunc is not
+ * NULL, *formatterfunc is set to the formatter function OID.
+ */
+Oid
+get_formatter_for_cast(Oid sourcetypeid, Oid targettypeid, Oid *formatterfunc)
+{
+ HeapTuple tp;
+ Oid result;
+
+ tp = SearchSysCache2(FORMATTERSOURCETARGET,
+ ObjectIdGetDatum(sourcetypeid),
+ ObjectIdGetDatum(targettypeid));
+ if (!HeapTupleIsValid(tp))
+ return InvalidOid;
+
+ {
+ Form_pg_formatter fmt = (Form_pg_formatter) GETSTRUCT(tp);
+
+ /* A valid pg_formatter row always names a formatter function. */
+ Assert(OidIsValid(fmt->fmtfunc));
+
+ result = fmt->oid;
+ if (formatterfunc != NULL)
+ *formatterfunc = fmt->fmtfunc;
+ }
+ ReleaseSysCache(tp);
+ return result;
+}
+
/* ---------- COLLATION CACHE ---------- */
/*
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index bb05aeebee4..f5752fefee3 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1230,6 +1230,44 @@ typedef struct CoerceViaIO
ParseLoc location; /* token location, or -1 if unknown */
} CoerceViaIO;
+/* ----------------
+ * CoerceViaFormatter
+ *
+ * CoerceViaFormatter represents CAST(arg AS resulttype FORMAT format), a
+ * formatted cast resolved through the pg_formatter catalog. It calls the
+ * registered formatter function with the source value and the FORMAT
+ * expression (already coerced to text), returning the target type:
+ *
+ * formatterfunc(arg, format) returns resulttype
+ *
+ * Unlike a plain FuncExpr, this node remembers that it came from
+ * CAST ... FORMAT (so it deparses back to that syntax) and which
+ * pg_formatter row it resolved to (so a stored expression can depend on the
+ * formatter object). It is executed by reusing ordinary function-call
+ * evaluation, so EXECUTE permission on formatterfunc is checked at run time,
+ * as for an ordinary function-backed cast.
+ * ----------------
+ */
+typedef struct CoerceViaFormatter
+{
+ Expr xpr;
+ Expr *arg; /* source expression */
+ Expr *format; /* FORMAT expression, already coerced to text */
+ Oid resulttype; /* output type of the cast */
+ /* output typmod (usually -1; typmod enforcement is layered above) */
+ int32 resulttypmod pg_node_attr(query_jumble_ignore);
+ /* OID of result collation, or InvalidOid if none */
+ Oid resultcollid pg_node_attr(query_jumble_ignore);
+ /* input collation for the formatter function call */
+ Oid inputcollid pg_node_attr(query_jumble_ignore);
+ Oid formatterfunc; /* pg_formatter.fmtfunc */
+ /* pg_formatter row OID this resolved to (for dependencies) */
+ Oid formatterid pg_node_attr(query_jumble_ignore);
+ /* how to display this node */
+ CoercionForm coercionformat pg_node_attr(query_jumble_ignore);
+ ParseLoc location; /* token location, or -1 if unknown */
+} CoerceViaFormatter;
+
/* ----------------
* ArrayCoerceExpr
*
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 865980cb0f1..c946bf6f59b 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -103,6 +103,8 @@ extern void get_atttypetypmodcoll(Oid relid, AttrNumber attnum,
Oid *typid, int32 *typmod, Oid *collid);
extern Datum get_attoptions(Oid relid, int16 attnum);
extern Oid get_cast_oid(Oid sourcetypeid, Oid targettypeid, bool missing_ok);
+extern Oid get_formatter_for_cast(Oid sourcetypeid, Oid targettypeid,
+ Oid *formatterfunc);
extern char *get_collation_name(Oid colloid);
extern bool get_collation_isdeterministic(Oid colloid);
extern char *get_constraint_name(Oid conoid);
diff --git a/src/test/regress/expected/expressions.out b/src/test/regress/expected/expressions.out
index b2d71dca4fa..7d064024b3a 100644
--- a/src/test/regress/expected/expressions.out
+++ b/src/test/regress/expected/expressions.out
@@ -564,23 +564,27 @@ 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
+-- A FORMAT clause is resolved through a registered formatter (see CREATE
+-- FORMATTER) and never falls back to an ordinary cast. No formatters are
+-- defined in this test, so these casts fail with a missing-formatter error;
+-- this confirms the FORMAT clause is neither ignored nor treated as an
+-- ordinary cast. An unknown-type source literal is coerced to text first,
+-- so the lookup key is (text, target).
+-- basic form (looks up a formatter for (text, date))
SELECT CAST('2026-06-24' AS date FORMAT 'YYYY-MM-DD');
-ERROR: formatted casts are not implemented yet
+ERROR: formatter for cast from type text to type date does not exist
LINE 1: SELECT CAST('2026-06-24' AS date FORMAT 'YYYY-MM-DD');
- ^
-DETAIL: No formatter resolution mechanism is available.
+ ^
+HINT: Use CREATE FORMATTER to define a formatter for this type pair.
-- 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
+ERROR: formatter for cast from type text to type date does not exist
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
+ ^
+HINT: Use CREATE FORMATTER to define a formatter for this type pair.
+-- a no-op-looking cast must not be relabeled away; it needs a (text, text) formatter
SELECT CAST('abc'::text AS text FORMAT 'whatever');
-ERROR: formatted casts are not implemented yet
+ERROR: formatter for cast from type text to type text does not exist
LINE 1: SELECT CAST('abc'::text AS text FORMAT 'whatever');
- ^
-DETAIL: No formatter resolution mechanism is available.
+ ^
+HINT: Use CREATE FORMATTER to define a formatter for this type pair.
diff --git a/src/test/regress/expected/formatters.out b/src/test/regress/expected/formatters.out
index bb1f091a1b8..6a47f007f12 100644
--- a/src/test/regress/expected/formatters.out
+++ b/src/test/regress/expected/formatters.out
@@ -1,9 +1,10 @@
--
-- FORMATTERS
--
--- CREATE/DROP FORMATTER registers formatter metadata in pg_formatter,
--- keyed by a (source type, target type) pair. This is catalog and DDL
--- infrastructure only; it does not transform or execute formatted casts.
+-- A formatter registers a function for a (source type, target type) pair; a
+-- CAST(... AS target FORMAT format_expr) is resolved through pg_formatter and
+-- calls that function. This test covers both the CREATE/DROP FORMATTER
+-- catalog DDL and the execution of formatted casts.
-- A simple formatter function with the required signature
-- formatter(source_type, text) returns target_type
CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text
@@ -64,14 +65,124 @@ CREATE FUNCTION fmt_anyel(anyelement, text) RETURNS text
CREATE FORMATTER FOR CAST (anyelement AS text)
WITH FUNCTION fmt_anyel(anyelement, text);
ERROR: source data type anyelement is a pseudo-type
--- Registering a formatter does not enable a formatted cast: the FORMAT
--- clause must not be silently ignored or rewritten to a built-in function,
--- so CAST(... FORMAT ...) is rejected during parse analysis.
-SELECT CAST(5 AS text FORMAT 'YYYY');
-ERROR: formatted casts are not implemented yet
-LINE 1: SELECT CAST(5 AS text FORMAT 'YYYY');
+-- ====================================================================
+-- Execution: a formatted cast resolves through pg_formatter and calls the
+-- registered formatter function.
+-- ====================================================================
+-- basic execution, using the (integer, text) formatter created above
+SELECT CAST(5 AS text FORMAT 'abc');
+ text
+-------
+ 5:abc
+(1 row)
+
+-- the FORMAT expression may be any expression, coerced to text
+SELECT CAST(5 AS text FORMAT 'a' || 'b');
+ text
+------
+ 5:ab
+(1 row)
+
+SELECT CAST(5 AS text FORMAT 123);
+ text
+-------
+ 5:123
+(1 row)
+
+-- The FORMAT expression is parse-analyzed independently of (and before) the
+-- formatter lookup, so an invalid FORMAT expression reports a normal error.
+SELECT CAST(5 AS text FORMAT no_such_column);
+ERROR: column "no_such_column" does not exist
+LINE 1: SELECT CAST(5 AS text FORMAT no_such_column);
^
-DETAIL: No formatter resolution mechanism is available.
+-- Like an ordinary cast that uses a cast function, a formatted cast checks
+-- EXECUTE on the formatter function at use time.
+REVOKE EXECUTE ON FUNCTION int4_to_text_fmt(integer, text) FROM PUBLIC;
+CREATE ROLE regress_formatter_noexec NOLOGIN;
+SET ROLE regress_formatter_noexec;
+SELECT CAST(5 AS text FORMAT 'p'); -- fails: no EXECUTE on formatter function
+ERROR: permission denied for function int4_to_text_fmt
+RESET ROLE;
+DROP ROLE regress_formatter_noexec;
+GRANT EXECUTE ON FUNCTION int4_to_text_fmt(integer, text) TO PUBLIC;
+-- A FORMAT clause never falls back to an ordinary cast: text -> text is a
+-- trivial ordinary cast, but a formatted cast still requires a formatter.
+SELECT CAST('abc'::text AS text FORMAT 'whatever');
+ERROR: formatter for cast from type text to type text does not exist
+LINE 1: SELECT CAST('abc'::text AS text FORMAT 'whatever');
+ ^
+HINT: Use CREATE FORMATTER to define a formatter for this type pair.
+-- An unknown-type source literal is coerced to text first, so this also looks
+-- up (text, text), not (unknown, text).
+SELECT CAST('abc' AS text FORMAT 'fmt');
+ERROR: formatter for cast from type text to type text does not exist
+LINE 1: SELECT CAST('abc' AS text FORMAT 'fmt');
+ ^
+HINT: Use CREATE FORMATTER to define a formatter for this type pair.
+-- A missing formatter is an error even where an ordinary cast would be valid.
+SELECT CAST(5 AS integer FORMAT 'x');
+ERROR: formatter for cast from type integer to type integer does not exist
+LINE 1: SELECT CAST(5 AS integer FORMAT 'x');
+ ^
+HINT: Use CREATE FORMATTER to define a formatter for this type pair.
+-- Define the (text, text) formatter and re-run the text-source cases.
+CREATE FUNCTION text_to_text_fmt(text, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1 || '/' || $2;
+CREATE FORMATTER FOR CAST (text AS text)
+ WITH FUNCTION text_to_text_fmt(text, text);
+SELECT CAST('abc'::text AS text FORMAT 'whatever');
+ text
+--------------
+ abc/whatever
+(1 row)
+
+SELECT CAST('abc' AS text FORMAT 'fmt');
+ text
+---------
+ abc/fmt
+(1 row)
+
+-- A type modifier on the target is enforced through the ordinary coercion path.
+CREATE FUNCTION int4_to_vc_fmt(integer, text) RETURNS varchar
+ LANGUAGE sql IMMUTABLE RETURN $1::text || $2;
+CREATE FORMATTER FOR CAST (integer AS varchar)
+ WITH FUNCTION int4_to_vc_fmt(integer, text);
+SELECT CAST(5 AS varchar FORMAT 'XXXX');
+ varchar
+---------
+ 5XXXX
+(1 row)
+
+SELECT CAST(5 AS varchar(3) FORMAT 'XXXX'); -- length 3 enforced
+ varchar
+---------
+ 5XX
+(1 row)
+
+-- Domain target: the formatter must return the domain type, so the domain's
+-- constraints are enforced by the function's result.
+CREATE DOMAIN nonempty_text AS text CHECK (VALUE <> '');
+CREATE FUNCTION text_to_netext_fmt(text, text) RETURNS nonempty_text
+ LANGUAGE sql IMMUTABLE RETURN $2::nonempty_text;
+CREATE FORMATTER FOR CAST (text AS nonempty_text)
+ WITH FUNCTION text_to_netext_fmt(text, text);
+SELECT CAST('z'::text AS nonempty_text FORMAT 'ok');
+ nonempty_text
+---------------
+ ok
+(1 row)
+
+SELECT CAST('z'::text AS nonempty_text FORMAT ''); -- domain check violation
+ERROR: value for domain nonempty_text violates check constraint "nonempty_text_check"
+CONTEXT: SQL function "text_to_netext_fmt" statement 1
+-- Drop the objects created in this execution section.
+DROP FORMATTER FOR CAST (text AS text);
+DROP FUNCTION text_to_text_fmt(text, text);
+DROP FORMATTER FOR CAST (integer AS varchar);
+DROP FUNCTION int4_to_vc_fmt(integer, text);
+DROP FORMATTER FOR CAST (text AS nonempty_text);
+DROP FUNCTION text_to_netext_fmt(text, text);
+DROP DOMAIN nonempty_text;
-- Dependency behavior: the formatter depends on its function.
DROP FUNCTION int4_to_text_fmt(integer, text); -- fails (RESTRICT)
ERROR: cannot drop function int4_to_text_fmt(integer,text) because other objects depend on it
@@ -114,3 +225,135 @@ DROP FUNCTION fmt_bad_arg1(bigint, text);
DROP FUNCTION fmt_bad_ret(integer, text);
DROP FUNCTION fmt_bad_set(integer, text);
DROP FUNCTION fmt_anyel(anyelement, text);
+-- ====================================================================
+-- CoerceViaFormatter: deparse fidelity and dependency on the formatter row
+-- ====================================================================
+CREATE FUNCTION i2t_view(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2;
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION i2t_view(integer, text);
+-- A view over a formatted cast deparses back to CAST(... FORMAT ...).
+CREATE VIEW formatter_view AS SELECT CAST(5 AS text FORMAT 'abc') AS x;
+SELECT pg_get_viewdef('formatter_view'::regclass, true);
+ pg_get_viewdef
+--------------------------------------------------
+ SELECT CAST(5 AS text FORMAT 'abc'::text) AS x;
+(1 row)
+
+SELECT * FROM formatter_view;
+ x
+-------
+ 5:abc
+(1 row)
+
+-- The view depends on the pg_formatter row, so DROP FORMATTER RESTRICT fails.
+DROP FORMATTER FOR CAST (integer AS text); -- fails (RESTRICT, view depends)
+ERROR: cannot drop formatter for cast from integer to text because other objects depend on it
+DETAIL: view formatter_view depends on formatter for cast from integer to text
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+-- ... and CASCADE drops the dependent view.
+DROP FORMATTER FOR CAST (integer AS text) CASCADE;
+NOTICE: drop cascades to view formatter_view
+SELECT count(*) FROM pg_class WHERE relname = 'formatter_view';
+ count
+-------
+ 0
+(1 row)
+
+DROP FUNCTION i2t_view(integer, text);
+-- ====================================================================
+-- Collation: a formatted cast behaves like calling the formatter function
+-- formatterfunc(arg, format). Its result collation is the result type's
+-- collation, and the input collation is derived from the arg and FORMAT
+-- expressions exactly as for an ordinary two-argument function call.
+-- ====================================================================
+-- text_larger(text, text) returns text and is collation-sensitive at the C
+-- level (it reads PG_GET_COLLATION), so it exercises the input collation.
+CREATE FORMATTER FOR CAST (text AS text)
+ WITH FUNCTION pg_catalog.text_larger(text, text);
+-- The default collation flows into the formatter call; if the input collation
+-- were left unset this would fail with "could not determine which collation".
+SELECT CAST('apple'::text AS text FORMAT 'banana'::text);
+ text
+--------
+ banana
+(1 row)
+
+-- An explicit COLLATE on an operand is honored as the input collation.
+SELECT CAST('apple'::text AS text FORMAT 'banana'::text COLLATE "C");
+ text
+--------
+ banana
+(1 row)
+
+-- The result is collatable, so an explicit COLLATE on the cast itself works.
+SELECT CAST('apple'::text AS text FORMAT 'banana'::text) COLLATE "C" < 'zzz';
+ ?column?
+----------
+ t
+(1 row)
+
+-- Conflicting explicit input collations are rejected, just like a plain
+-- function call text_larger('a' COLLATE "C", 'b' COLLATE "POSIX").
+SELECT CAST('a'::text COLLATE "C" AS text FORMAT 'b'::text COLLATE "POSIX");
+ERROR: collation mismatch between explicit collations "C" and "POSIX"
+LINE 1: ...ST('a'::text COLLATE "C" AS text FORMAT 'b'::text COLLATE "P...
+ ^
+DROP FORMATTER FOR CAST (text AS text);
+-- ====================================================================
+-- ruleutils: arg and FORMAT subexpressions deparse unambiguously inside
+-- CAST(...), needing no extra parentheses, and re-parse to the same thing.
+-- ====================================================================
+CREATE FUNCTION i2t_paren(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || $2;
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION i2t_paren(integer, text);
+CREATE VIEW formatter_paren_view AS
+ SELECT CAST((1 + 2) AS text FORMAT ('a' || 'b')) AS x;
+SELECT pg_get_viewdef('formatter_paren_view'::regclass, true);
+ pg_get_viewdef
+-----------------------------------------------------------------
+ SELECT CAST(1 + 2 AS text FORMAT 'a'::text || 'b'::text) AS x;
+(1 row)
+
+SELECT * FROM formatter_paren_view;
+ x
+-----
+ 3ab
+(1 row)
+
+DROP VIEW formatter_paren_view;
+DROP FORMATTER FOR CAST (integer AS text);
+DROP FUNCTION i2t_paren(integer, text);
+-- ====================================================================
+-- Dependency completeness: a view over a formatted cast depends (through the
+-- pg_formatter row) on the formatter function, so dropping the function with
+-- CASCADE removes both the formatter row and the view in one step.
+-- ====================================================================
+CREATE FUNCTION i2t_chain(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || $2;
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION i2t_chain(integer, text);
+CREATE VIEW formatter_chain_view AS SELECT CAST(7 AS text FORMAT 'q') AS x;
+DROP FUNCTION i2t_chain(integer, text); -- fails: formatter + view depend
+ERROR: cannot drop function i2t_chain(integer,text) because other objects depend on it
+DETAIL: formatter for cast from integer to text depends on function i2t_chain(integer,text)
+view formatter_chain_view depends on function i2t_chain(integer,text)
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+DROP FUNCTION i2t_chain(integer, text) CASCADE; -- drops formatter and view
+NOTICE: drop cascades to 2 other objects
+DETAIL: drop cascades to formatter for cast from integer to text
+drop cascades to view formatter_chain_view
+SELECT count(*) FROM pg_formatter
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype;
+ count
+-------
+ 0
+(1 row)
+
+SELECT count(*) FROM pg_class WHERE relname = 'formatter_chain_view';
+ count
+-------
+ 0
+(1 row)
+
diff --git a/src/test/regress/sql/expressions.sql b/src/test/regress/sql/expressions.sql
index fb05990ed2d..46575bc410d 100644
--- a/src/test/regress/sql/expressions.sql
+++ b/src/test/regress/sql/expressions.sql
@@ -305,14 +305,18 @@ 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
+-- A FORMAT clause is resolved through a registered formatter (see CREATE
+-- FORMATTER) and never falls back to an ordinary cast. No formatters are
+-- defined in this test, so these casts fail with a missing-formatter error;
+-- this confirms the FORMAT clause is neither ignored nor treated as an
+-- ordinary cast. An unknown-type source literal is coerced to text first,
+-- so the lookup key is (text, target).
+
+-- basic form (looks up a formatter for (text, date))
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
+-- a no-op-looking cast must not be relabeled away; it needs a (text, text) formatter
SELECT CAST('abc'::text AS text FORMAT 'whatever');
diff --git a/src/test/regress/sql/formatters.sql b/src/test/regress/sql/formatters.sql
index 8050aee6f45..2c9b4351652 100644
--- a/src/test/regress/sql/formatters.sql
+++ b/src/test/regress/sql/formatters.sql
@@ -1,9 +1,10 @@
--
-- FORMATTERS
--
--- CREATE/DROP FORMATTER registers formatter metadata in pg_formatter,
--- keyed by a (source type, target type) pair. This is catalog and DDL
--- infrastructure only; it does not transform or execute formatted casts.
+-- A formatter registers a function for a (source type, target type) pair; a
+-- CAST(... AS target FORMAT format_expr) is resolved through pg_formatter and
+-- calls that function. This test covers both the CREATE/DROP FORMATTER
+-- catalog DDL and the execution of formatted casts.
-- A simple formatter function with the required signature
-- formatter(source_type, text) returns target_type
@@ -59,10 +60,74 @@ CREATE FUNCTION fmt_anyel(anyelement, text) RETURNS text
CREATE FORMATTER FOR CAST (anyelement AS text)
WITH FUNCTION fmt_anyel(anyelement, text);
--- Registering a formatter does not enable a formatted cast: the FORMAT
--- clause must not be silently ignored or rewritten to a built-in function,
--- so CAST(... FORMAT ...) is rejected during parse analysis.
-SELECT CAST(5 AS text FORMAT 'YYYY');
+-- ====================================================================
+-- Execution: a formatted cast resolves through pg_formatter and calls the
+-- registered formatter function.
+-- ====================================================================
+
+-- basic execution, using the (integer, text) formatter created above
+SELECT CAST(5 AS text FORMAT 'abc');
+-- the FORMAT expression may be any expression, coerced to text
+SELECT CAST(5 AS text FORMAT 'a' || 'b');
+SELECT CAST(5 AS text FORMAT 123);
+
+-- The FORMAT expression is parse-analyzed independently of (and before) the
+-- formatter lookup, so an invalid FORMAT expression reports a normal error.
+SELECT CAST(5 AS text FORMAT no_such_column);
+
+-- Like an ordinary cast that uses a cast function, a formatted cast checks
+-- EXECUTE on the formatter function at use time.
+REVOKE EXECUTE ON FUNCTION int4_to_text_fmt(integer, text) FROM PUBLIC;
+CREATE ROLE regress_formatter_noexec NOLOGIN;
+SET ROLE regress_formatter_noexec;
+SELECT CAST(5 AS text FORMAT 'p'); -- fails: no EXECUTE on formatter function
+RESET ROLE;
+DROP ROLE regress_formatter_noexec;
+GRANT EXECUTE ON FUNCTION int4_to_text_fmt(integer, text) TO PUBLIC;
+
+-- A FORMAT clause never falls back to an ordinary cast: text -> text is a
+-- trivial ordinary cast, but a formatted cast still requires a formatter.
+SELECT CAST('abc'::text AS text FORMAT 'whatever');
+-- An unknown-type source literal is coerced to text first, so this also looks
+-- up (text, text), not (unknown, text).
+SELECT CAST('abc' AS text FORMAT 'fmt');
+-- A missing formatter is an error even where an ordinary cast would be valid.
+SELECT CAST(5 AS integer FORMAT 'x');
+
+-- Define the (text, text) formatter and re-run the text-source cases.
+CREATE FUNCTION text_to_text_fmt(text, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1 || '/' || $2;
+CREATE FORMATTER FOR CAST (text AS text)
+ WITH FUNCTION text_to_text_fmt(text, text);
+SELECT CAST('abc'::text AS text FORMAT 'whatever');
+SELECT CAST('abc' AS text FORMAT 'fmt');
+
+-- A type modifier on the target is enforced through the ordinary coercion path.
+CREATE FUNCTION int4_to_vc_fmt(integer, text) RETURNS varchar
+ LANGUAGE sql IMMUTABLE RETURN $1::text || $2;
+CREATE FORMATTER FOR CAST (integer AS varchar)
+ WITH FUNCTION int4_to_vc_fmt(integer, text);
+SELECT CAST(5 AS varchar FORMAT 'XXXX');
+SELECT CAST(5 AS varchar(3) FORMAT 'XXXX'); -- length 3 enforced
+
+-- Domain target: the formatter must return the domain type, so the domain's
+-- constraints are enforced by the function's result.
+CREATE DOMAIN nonempty_text AS text CHECK (VALUE <> '');
+CREATE FUNCTION text_to_netext_fmt(text, text) RETURNS nonempty_text
+ LANGUAGE sql IMMUTABLE RETURN $2::nonempty_text;
+CREATE FORMATTER FOR CAST (text AS nonempty_text)
+ WITH FUNCTION text_to_netext_fmt(text, text);
+SELECT CAST('z'::text AS nonempty_text FORMAT 'ok');
+SELECT CAST('z'::text AS nonempty_text FORMAT ''); -- domain check violation
+
+-- Drop the objects created in this execution section.
+DROP FORMATTER FOR CAST (text AS text);
+DROP FUNCTION text_to_text_fmt(text, text);
+DROP FORMATTER FOR CAST (integer AS varchar);
+DROP FUNCTION int4_to_vc_fmt(integer, text);
+DROP FORMATTER FOR CAST (text AS nonempty_text);
+DROP FUNCTION text_to_netext_fmt(text, text);
+DROP DOMAIN nonempty_text;
-- Dependency behavior: the formatter depends on its function.
DROP FUNCTION int4_to_text_fmt(integer, text); -- fails (RESTRICT)
@@ -97,3 +162,77 @@ DROP FUNCTION fmt_bad_arg1(bigint, text);
DROP FUNCTION fmt_bad_ret(integer, text);
DROP FUNCTION fmt_bad_set(integer, text);
DROP FUNCTION fmt_anyel(anyelement, text);
+
+-- ====================================================================
+-- CoerceViaFormatter: deparse fidelity and dependency on the formatter row
+-- ====================================================================
+CREATE FUNCTION i2t_view(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2;
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION i2t_view(integer, text);
+
+-- A view over a formatted cast deparses back to CAST(... FORMAT ...).
+CREATE VIEW formatter_view AS SELECT CAST(5 AS text FORMAT 'abc') AS x;
+SELECT pg_get_viewdef('formatter_view'::regclass, true);
+SELECT * FROM formatter_view;
+
+-- The view depends on the pg_formatter row, so DROP FORMATTER RESTRICT fails.
+DROP FORMATTER FOR CAST (integer AS text); -- fails (RESTRICT, view depends)
+-- ... and CASCADE drops the dependent view.
+DROP FORMATTER FOR CAST (integer AS text) CASCADE;
+SELECT count(*) FROM pg_class WHERE relname = 'formatter_view';
+DROP FUNCTION i2t_view(integer, text);
+
+-- ====================================================================
+-- Collation: a formatted cast behaves like calling the formatter function
+-- formatterfunc(arg, format). Its result collation is the result type's
+-- collation, and the input collation is derived from the arg and FORMAT
+-- expressions exactly as for an ordinary two-argument function call.
+-- ====================================================================
+-- text_larger(text, text) returns text and is collation-sensitive at the C
+-- level (it reads PG_GET_COLLATION), so it exercises the input collation.
+CREATE FORMATTER FOR CAST (text AS text)
+ WITH FUNCTION pg_catalog.text_larger(text, text);
+-- The default collation flows into the formatter call; if the input collation
+-- were left unset this would fail with "could not determine which collation".
+SELECT CAST('apple'::text AS text FORMAT 'banana'::text);
+-- An explicit COLLATE on an operand is honored as the input collation.
+SELECT CAST('apple'::text AS text FORMAT 'banana'::text COLLATE "C");
+-- The result is collatable, so an explicit COLLATE on the cast itself works.
+SELECT CAST('apple'::text AS text FORMAT 'banana'::text) COLLATE "C" < 'zzz';
+-- Conflicting explicit input collations are rejected, just like a plain
+-- function call text_larger('a' COLLATE "C", 'b' COLLATE "POSIX").
+SELECT CAST('a'::text COLLATE "C" AS text FORMAT 'b'::text COLLATE "POSIX");
+DROP FORMATTER FOR CAST (text AS text);
+
+-- ====================================================================
+-- ruleutils: arg and FORMAT subexpressions deparse unambiguously inside
+-- CAST(...), needing no extra parentheses, and re-parse to the same thing.
+-- ====================================================================
+CREATE FUNCTION i2t_paren(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || $2;
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION i2t_paren(integer, text);
+CREATE VIEW formatter_paren_view AS
+ SELECT CAST((1 + 2) AS text FORMAT ('a' || 'b')) AS x;
+SELECT pg_get_viewdef('formatter_paren_view'::regclass, true);
+SELECT * FROM formatter_paren_view;
+DROP VIEW formatter_paren_view;
+DROP FORMATTER FOR CAST (integer AS text);
+DROP FUNCTION i2t_paren(integer, text);
+
+-- ====================================================================
+-- Dependency completeness: a view over a formatted cast depends (through the
+-- pg_formatter row) on the formatter function, so dropping the function with
+-- CASCADE removes both the formatter row and the view in one step.
+-- ====================================================================
+CREATE FUNCTION i2t_chain(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || $2;
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION i2t_chain(integer, text);
+CREATE VIEW formatter_chain_view AS SELECT CAST(7 AS text FORMAT 'q') AS x;
+DROP FUNCTION i2t_chain(integer, text); -- fails: formatter + view depend
+DROP FUNCTION i2t_chain(integer, text) CASCADE; -- drops formatter and view
+SELECT count(*) FROM pg_formatter
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype;
+SELECT count(*) FROM pg_class WHERE relname = 'formatter_chain_view';
--
2.54.0
[application/octet-stream] 0001-Add-parser-support-for-CAST-FORMAT-syntax.patch (5.6K, ../../CABXr29FyPC7terFF7E+r462BEHhYgv06oUVoBrhkH7xhshuE6A@mail.gmail.com/4-0001-Add-parser-support-for-CAST-FORMAT-syntax.patch)
download | inline diff:
From 8bb73db3ae0fdec4c0b53d0c3b67a2c3340d91de Mon Sep 17 00:00:00 2001
From: Haibo Yan <[email protected]>
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
[application/octet-stream] 0002-Add-pg_formatter-catalog-and-CREATE-DROP-FORMATTER.patch (69.4K, ../../CABXr29FyPC7terFF7E+r462BEHhYgv06oUVoBrhkH7xhshuE6A@mail.gmail.com/5-0002-Add-pg_formatter-catalog-and-CREATE-DROP-FORMATTER.patch)
download | inline diff:
From 70d36e8fd1387233cc01f5aa130bdc8f8d3ccd13 Mon Sep 17 00:00:00 2001
From: Haibo Yan <[email protected]>
Date: Wed, 24 Jun 2026 14:52:46 -0700
Subject: [PATCH] Add pg_formatter and CREATE/DROP FORMATTER
Add pg_formatter, a catalog for registering formatted conversions by
source and target type. A formatter function has the signature
formatter(source_type, text) returns target_type
where the second argument receives the FORMAT expression coerced to text.
Add CREATE FORMATTER and DROP FORMATTER, syscache support, object-address
and dependency handling, and pg_dump support. Formatter objects are
separate from pg_cast because formatted casts are always explicit,
function-backed, and keyed by a source/target type pair rather than by
ordinary cast semantics such as implicit, assignment, binary, or in/out
casts.
This patch only adds catalog and DDL infrastructure. CAST ... FORMAT
execution is added separately.
---
doc/src/sgml/catalogs.sgml | 83 ++++++++
doc/src/sgml/ref/allfiles.sgml | 2 +
doc/src/sgml/ref/create_formatter.sgml | 131 ++++++++++++
doc/src/sgml/ref/drop_formatter.sgml | 126 ++++++++++++
doc/src/sgml/reference.sgml | 2 +
src/backend/catalog/aclchk.c | 2 +
src/backend/catalog/dependency.c | 2 +
src/backend/catalog/objectaddress.c | 105 ++++++++++
src/backend/commands/Makefile | 1 +
src/backend/commands/dropcmds.c | 22 ++
src/backend/commands/event_trigger.c | 2 +
src/backend/commands/formattercmds.c | 245 +++++++++++++++++++++++
src/backend/commands/meson.build | 1 +
src/backend/commands/seclabel.c | 1 +
src/backend/parser/gram.y | 40 +++-
src/backend/tcop/utility.c | 17 ++
src/bin/pg_dump/common.c | 3 +
src/bin/pg_dump/pg_dump.c | 166 +++++++++++++++
src/bin/pg_dump/pg_dump.h | 10 +
src/bin/pg_dump/pg_dump_sort.c | 9 +
src/include/catalog/Makefile | 1 +
src/include/catalog/catversion.h | 2 +-
src/include/catalog/meson.build | 1 +
src/include/catalog/pg_formatter.h | 62 ++++++
src/include/commands/formatter.h | 24 +++
src/include/nodes/parsenodes.h | 17 ++
src/include/parser/kwlist.h | 1 +
src/include/tcop/cmdtaglist.h | 2 +
src/test/regress/expected/formatters.out | 116 +++++++++++
src/test/regress/expected/oidjoins.out | 3 +
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/formatters.sql | 99 +++++++++
src/tools/pgindent/typedefs.list | 4 +
33 files changed, 1300 insertions(+), 4 deletions(-)
create mode 100644 doc/src/sgml/ref/create_formatter.sgml
create mode 100644 doc/src/sgml/ref/drop_formatter.sgml
create mode 100644 src/backend/commands/formattercmds.c
create mode 100644 src/include/catalog/pg_formatter.h
create mode 100644 src/include/commands/formatter.h
create mode 100644 src/test/regress/expected/formatters.out
create mode 100644 src/test/regress/sql/formatters.sql
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 4b474c13917..75f4e4840fd 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -170,6 +170,11 @@
<entry>additional foreign table information</entry>
</row>
+ <row>
+ <entry><link linkend="catalog-pg-formatter"><structname>pg_formatter</structname></link></entry>
+ <entry>formatter functions for formatted casts</entry>
+ </row>
+
<row>
<entry><link linkend="catalog-pg-index"><structname>pg_index</structname></link></entry>
<entry>additional index information</entry>
@@ -4391,6 +4396,84 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</sect1>
+ <sect1 id="catalog-pg-formatter">
+ <title><structname>pg_formatter</structname></title>
+
+ <indexterm zone="catalog-pg-formatter">
+ <primary>pg_formatter</primary>
+ </indexterm>
+
+ <para>
+ The catalog <structname>pg_formatter</structname> stores formatting
+ conversion functions for formatted casts. A formatter is identified by a
+ source type and a target type. The associated function is called with the
+ source value and the <literal>FORMAT</literal> expression coerced to
+ <type>text</type>, and returns the target type. At most one formatter
+ exists for any given pair of source and target types. See <xref
+ linkend="sql-createformatter"/> for more information.
+ </para>
+
+ <table>
+ <title><structname>pg_formatter</structname> Columns</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>oid</structfield> <type>oid</type>
+ </para>
+ <para>
+ Row identifier
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>fmtsource</structfield> <type>oid</type>
+ (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>)
+ </para>
+ <para>
+ OID of the source data type of the formatted cast
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>fmttarget</structfield> <type>oid</type>
+ (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>)
+ </para>
+ <para>
+ OID of the target data type of the formatted cast
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>fmtfunc</structfield> <type>oid</type>
+ (references <link linkend="catalog-pg-proc"><structname>pg_proc</structname></link>.<structfield>oid</structfield>)
+ </para>
+ <para>
+ OID of the formatter function, which has the signature
+ <replaceable>function</replaceable>(<replaceable>source_type</replaceable>, <type>text</type>)
+ returning <replaceable>target_type</replaceable>
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect1>
+
+
<sect1 id="catalog-pg-index">
<title><structname>pg_index</structname></title>
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index e1a56c36221..b0bda54d87b 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -70,6 +70,7 @@ Complete list of usable sgml source files in this directory.
<!ENTITY createExtension SYSTEM "create_extension.sgml">
<!ENTITY createForeignDataWrapper SYSTEM "create_foreign_data_wrapper.sgml">
<!ENTITY createForeignTable SYSTEM "create_foreign_table.sgml">
+<!ENTITY createFormatter SYSTEM "create_formatter.sgml">
<!ENTITY createFunction SYSTEM "create_function.sgml">
<!ENTITY createGroup SYSTEM "create_group.sgml">
<!ENTITY createIndex SYSTEM "create_index.sgml">
@@ -118,6 +119,7 @@ Complete list of usable sgml source files in this directory.
<!ENTITY dropExtension SYSTEM "drop_extension.sgml">
<!ENTITY dropForeignDataWrapper SYSTEM "drop_foreign_data_wrapper.sgml">
<!ENTITY dropForeignTable SYSTEM "drop_foreign_table.sgml">
+<!ENTITY dropFormatter SYSTEM "drop_formatter.sgml">
<!ENTITY dropFunction SYSTEM "drop_function.sgml">
<!ENTITY dropGroup SYSTEM "drop_group.sgml">
<!ENTITY dropIndex SYSTEM "drop_index.sgml">
diff --git a/doc/src/sgml/ref/create_formatter.sgml b/doc/src/sgml/ref/create_formatter.sgml
new file mode 100644
index 00000000000..268b53e7d2c
--- /dev/null
+++ b/doc/src/sgml/ref/create_formatter.sgml
@@ -0,0 +1,131 @@
+<!--
+doc/src/sgml/ref/create_formatter.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-createformatter">
+ <indexterm zone="sql-createformatter">
+ <primary>CREATE FORMATTER</primary>
+ </indexterm>
+
+ <refmeta>
+ <refentrytitle>CREATE FORMATTER</refentrytitle>
+ <manvolnum>7</manvolnum>
+ <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+ <refname>CREATE FORMATTER</refname>
+ <refpurpose>define a new formatter for a formatted cast</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE FORMATTER FOR CAST (<replaceable>source_type</replaceable> AS <replaceable>target_type</replaceable>)
+ WITH FUNCTION <replaceable>function_name</replaceable> [ (<replaceable>argument_type</replaceable> [, ...]) ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1 id="sql-createformatter-description">
+ <title>Description</title>
+
+ <para>
+ <command>CREATE FORMATTER</command> defines a formatting conversion for
+ <literal>CAST(<replaceable>expr</replaceable> AS <replaceable>target_type</replaceable> FORMAT <replaceable>format_expr</replaceable>)</literal>.
+ A formatter is selected by the source type and target type of the cast.
+ Unlike an ordinary cast (see <xref linkend="sql-createcast"/>), a formatted
+ cast is always explicit and always uses a function, and the function
+ additionally receives the <literal>FORMAT</literal> expression.
+ </para>
+
+ <para>
+ The formatter function must take exactly two arguments and return the
+ target type:
+<synopsis>
+<replaceable>function_name</replaceable>(<replaceable>source_type</replaceable>, text) RETURNS <replaceable>target_type</replaceable>
+</synopsis>
+ The first argument is the value being formatted, and the second argument
+ receives the <literal>FORMAT</literal> expression coerced to
+ <type>text</type>. Only one formatter may be registered for a given
+ <literal>(<replaceable>source_type</replaceable>,
+ <replaceable>target_type</replaceable>)</literal> pair. Neither the source
+ type nor the target type may be a pseudo-type.
+ </para>
+
+ <para>
+ To create a formatter, you must be the owner of the source type or the
+ target type, and you must have <literal>EXECUTE</literal> privilege on the
+ formatter function.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>Parameters</title>
+
+ <variablelist>
+ <varlistentry>
+ <term><replaceable>source_type</replaceable></term>
+ <listitem>
+ <para>
+ The data type of the value passed to the formatter (the source of the
+ cast).
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable>target_type</replaceable></term>
+ <listitem>
+ <para>
+ The data type returned by the formatter (the target of the cast).
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable>function_name</replaceable>[(<replaceable>argument_type</replaceable> [, ...])]</term>
+ <listitem>
+ <para>
+ The function used to perform the formatted cast. The argument list, if
+ given, must name the two argument types
+ (<replaceable>source_type</replaceable> and <type>text</type>).
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+
+ <refsect1 id="sql-createformatter-examples">
+ <title>Examples</title>
+
+ <para>
+ Register a formatter that converts an <type>integer</type> to
+ <type>text</type> using the supplied format string:
+<programlisting>
+CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2;
+
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION int4_to_text_fmt(integer, text);
+</programlisting></para>
+ </refsect1>
+
+ <refsect1 id="sql-createformatter-compat">
+ <title>Compatibility</title>
+
+ <para>
+ <command>CREATE FORMATTER</command> is a
+ <productname>PostgreSQL</productname> extension.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>See Also</title>
+
+ <simplelist type="inline">
+ <member><xref linkend="sql-dropformatter"/></member>
+ <member><xref linkend="sql-createcast"/></member>
+ </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/drop_formatter.sgml b/doc/src/sgml/ref/drop_formatter.sgml
new file mode 100644
index 00000000000..b7f367a590e
--- /dev/null
+++ b/doc/src/sgml/ref/drop_formatter.sgml
@@ -0,0 +1,126 @@
+<!--
+doc/src/sgml/ref/drop_formatter.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-dropformatter">
+ <indexterm zone="sql-dropformatter">
+ <primary>DROP FORMATTER</primary>
+ </indexterm>
+
+ <refmeta>
+ <refentrytitle>DROP FORMATTER</refentrytitle>
+ <manvolnum>7</manvolnum>
+ <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+ <refname>DROP FORMATTER</refname>
+ <refpurpose>remove a formatter</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP FORMATTER [ IF EXISTS ] FOR CAST (<replaceable>source_type</replaceable> AS <replaceable>target_type</replaceable>) [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1 id="sql-dropformatter-description">
+ <title>Description</title>
+
+ <para>
+ <command>DROP FORMATTER</command> removes a previously defined formatter for
+ the given pair of source and target types.
+ </para>
+
+ <para>
+ Dropping a formatter requires ownership of either the source type or the
+ target type.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>Parameters</title>
+
+ <variablelist>
+ <varlistentry>
+ <term><literal>IF EXISTS</literal></term>
+ <listitem>
+ <para>
+ Do not throw an error if the formatter does not exist. A notice is
+ issued in this case.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable>source_type</replaceable></term>
+ <listitem>
+ <para>
+ The source data type of the formatter.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable>target_type</replaceable></term>
+ <listitem>
+ <para>
+ The target data type of the formatter.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>CASCADE</literal></term>
+ <listitem>
+ <para>
+ Automatically drop objects that depend on the formatter,
+ and in turn all objects that depend on those objects
+ (see <xref linkend="ddl-depend"/>).
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>RESTRICT</literal></term>
+ <listitem>
+ <para>
+ Refuse to drop the formatter if any objects depend on it. This is the
+ default.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+
+ <refsect1 id="sql-dropformatter-examples">
+ <title>Examples</title>
+
+ <para>
+ To drop the formatter for the cast from <type>integer</type> to
+ <type>text</type>:
+<programlisting>
+DROP FORMATTER FOR CAST (integer AS text);
+</programlisting></para>
+ </refsect1>
+
+ <refsect1 id="sql-dropformatter-compat">
+ <title>Compatibility</title>
+
+ <para>
+ <command>DROP FORMATTER</command> is a
+ <productname>PostgreSQL</productname> extension. See <xref
+ linkend="sql-createformatter"/> for details.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>See Also</title>
+
+ <simplelist type="inline">
+ <member><xref linkend="sql-createformatter"/></member>
+ </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index 674ac17e82c..a1d055509c8 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -98,6 +98,7 @@
&createExtension;
&createForeignDataWrapper;
&createForeignTable;
+ &createFormatter;
&createFunction;
&createGroup;
&createIndex;
@@ -146,6 +147,7 @@
&dropExtension;
&dropForeignDataWrapper;
&dropForeignTable;
+ &dropFormatter;
&dropFunction;
&dropGroup;
&dropIndex;
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..0f0211dd6c3 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -2794,6 +2794,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_AMPROC:
case OBJECT_ATTRIBUTE:
case OBJECT_CAST:
+ case OBJECT_FORMATTER:
case OBJECT_DEFAULT:
case OBJECT_DEFACL:
case OBJECT_DOMCONSTRAINT:
@@ -2937,6 +2938,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_AMPROC:
case OBJECT_ATTRIBUTE:
case OBJECT_CAST:
+ case OBJECT_FORMATTER:
case OBJECT_DEFAULT:
case OBJECT_DEFACL:
case OBJECT_DOMCONSTRAINT:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index c54774b3275..7eeac91264f 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -41,6 +41,7 @@
#include "catalog/pg_extension.h"
#include "catalog/pg_foreign_data_wrapper.h"
#include "catalog/pg_foreign_server.h"
+#include "catalog/pg_formatter.h"
#include "catalog/pg_init_privs.h"
#include "catalog/pg_language.h"
#include "catalog/pg_largeobject.h"
@@ -1534,6 +1535,7 @@ doDeletion(const ObjectAddress *object, int flags)
case DefaultAclRelationId:
case EventTriggerRelationId:
case TransformRelationId:
+ case FormatterRelationId:
case AuthMemRelationId:
DropObjectById(object);
break;
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index af0e4703616..fcf38db8c55 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -37,6 +37,7 @@
#include "catalog/pg_extension.h"
#include "catalog/pg_foreign_data_wrapper.h"
#include "catalog/pg_foreign_server.h"
+#include "catalog/pg_formatter.h"
#include "catalog/pg_language.h"
#include "catalog/pg_largeobject.h"
#include "catalog/pg_largeobject_metadata.h"
@@ -70,6 +71,7 @@
#include "commands/defrem.h"
#include "commands/event_trigger.h"
#include "commands/extension.h"
+#include "commands/formatter.h"
#include "commands/policy.h"
#include "commands/proclang.h"
#include "commands/tablespace.h"
@@ -179,6 +181,20 @@ static const ObjectPropertyType ObjectProperty[] =
OBJECT_CAST,
false
},
+ {
+ "formatter",
+ FormatterRelationId,
+ FormatterOidIndexId,
+ FORMATTEROID,
+ SYSCACHEID_INVALID,
+ Anum_pg_formatter_oid,
+ InvalidAttrNumber,
+ InvalidAttrNumber,
+ InvalidAttrNumber,
+ InvalidAttrNumber,
+ OBJECT_FORMATTER,
+ false
+ },
{
"collation",
CollationRelationId,
@@ -796,6 +812,9 @@ static const struct object_type_map
{
"cast", OBJECT_CAST
},
+ {
+ "formatter", OBJECT_FORMATTER
+ },
{
"collation", OBJECT_COLLATION
},
@@ -1165,6 +1184,21 @@ get_object_address(ObjectType objtype, Node *object,
address.objectSubId = 0;
}
break;
+ case OBJECT_FORMATTER:
+ {
+ TypeName *sourcetype = linitial_node(TypeName, castNode(List, object));
+ TypeName *targettype = lsecond_node(TypeName, castNode(List, object));
+ Oid sourcetypeid;
+ Oid targettypeid;
+
+ sourcetypeid = LookupTypeNameOid(NULL, sourcetype, missing_ok);
+ targettypeid = LookupTypeNameOid(NULL, targettype, missing_ok);
+ address.classId = FormatterRelationId;
+ address.objectId =
+ get_formatter_oid(sourcetypeid, targettypeid, missing_ok);
+ address.objectSubId = 0;
+ }
+ break;
case OBJECT_TRANSFORM:
{
TypeName *typename = linitial_node(TypeName, castNode(List, object));
@@ -2239,6 +2273,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
* exceptions.
*/
if (type == OBJECT_TYPE || type == OBJECT_DOMAIN || type == OBJECT_CAST ||
+ type == OBJECT_FORMATTER ||
type == OBJECT_TRANSFORM || type == OBJECT_DOMCONSTRAINT)
{
Datum *elems;
@@ -2291,6 +2326,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
type == OBJECT_ROUTINE ||
type == OBJECT_OPERATOR ||
type == OBJECT_CAST ||
+ type == OBJECT_FORMATTER ||
type == OBJECT_AMOP ||
type == OBJECT_AMPROC)
{
@@ -2336,6 +2372,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
pg_fallthrough;
case OBJECT_DOMCONSTRAINT:
case OBJECT_CAST:
+ case OBJECT_FORMATTER:
case OBJECT_PUBLICATION_REL:
case OBJECT_DEFACL:
case OBJECT_TRANSFORM:
@@ -2424,6 +2461,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
objnode = (Node *) typename;
break;
case OBJECT_CAST:
+ case OBJECT_FORMATTER:
case OBJECT_DOMCONSTRAINT:
case OBJECT_TRANSFORM:
objnode = (Node *) list_make2(typename, linitial(args));
@@ -2583,6 +2621,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
address.objectId)));
break;
case OBJECT_CAST:
+ case OBJECT_FORMATTER:
{
/* We can only check permissions on the source/target types */
TypeName *sourcetype = linitial_node(TypeName, castNode(List, object));
@@ -3111,6 +3150,31 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
break;
}
+ case FormatterRelationId:
+ {
+ HeapTuple fmtTup;
+ Form_pg_formatter fmtForm;
+
+ fmtTup = SearchSysCache1(FORMATTEROID,
+ ObjectIdGetDatum(object->objectId));
+ if (!HeapTupleIsValid(fmtTup))
+ {
+ if (!missing_ok)
+ elog(ERROR, "could not find tuple for formatter %u",
+ object->objectId);
+ break;
+ }
+
+ fmtForm = (Form_pg_formatter) GETSTRUCT(fmtTup);
+
+ appendStringInfo(&buffer, _("formatter for cast from %s to %s"),
+ format_type_be(fmtForm->fmtsource),
+ format_type_be(fmtForm->fmttarget));
+
+ ReleaseSysCache(fmtTup);
+ break;
+ }
+
case CollationRelationId:
{
HeapTuple collTup;
@@ -4762,6 +4826,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
appendStringInfoString(&buffer, "cast");
break;
+ case FormatterRelationId:
+ appendStringInfoString(&buffer, "formatter");
+ break;
+
case CollationRelationId:
appendStringInfoString(&buffer, "collation");
break;
@@ -5225,6 +5293,43 @@ getObjectIdentityParts(const ObjectAddress *object,
break;
}
+ case FormatterRelationId:
+ {
+ Relation fmtRel;
+ HeapTuple tup;
+ Form_pg_formatter fmtForm;
+
+ fmtRel = table_open(FormatterRelationId, AccessShareLock);
+
+ tup = get_catalog_object_by_oid(fmtRel, Anum_pg_formatter_oid,
+ object->objectId);
+
+ if (!HeapTupleIsValid(tup))
+ {
+ if (!missing_ok)
+ elog(ERROR, "could not find tuple for formatter %u",
+ object->objectId);
+
+ table_close(fmtRel, AccessShareLock);
+ break;
+ }
+
+ fmtForm = (Form_pg_formatter) GETSTRUCT(tup);
+
+ appendStringInfo(&buffer, "for cast (%s AS %s)",
+ format_type_be_qualified(fmtForm->fmtsource),
+ format_type_be_qualified(fmtForm->fmttarget));
+
+ if (objname)
+ {
+ *objname = list_make1(format_type_be_qualified(fmtForm->fmtsource));
+ *objargs = list_make1(format_type_be_qualified(fmtForm->fmttarget));
+ }
+
+ table_close(fmtRel, AccessShareLock);
+ break;
+ }
+
case CollationRelationId:
{
HeapTuple collTup;
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 5b9d084977e..7a4066867e0 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -38,6 +38,7 @@ OBJS = \
explain_state.o \
extension.o \
foreigncmds.o \
+ formattercmds.o \
functioncmds.o \
indexcmds.o \
lockcmds.o \
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 88a2df65c69..90e250f85ba 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -25,6 +25,7 @@
#include "miscadmin.h"
#include "parser/parse_type.h"
#include "utils/acl.h"
+#include "utils/builtins.h"
#include "utils/lsyscache.h"
@@ -401,6 +402,27 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
}
}
break;
+ case OBJECT_FORMATTER:
+ {
+ if (!type_in_list_does_not_exist_skipping(list_make1(linitial(castNode(List, object))), &msg, &name) &&
+ !type_in_list_does_not_exist_skipping(list_make1(lsecond(castNode(List, object))), &msg, &name))
+ {
+ /*
+ * Both types exist (else the checks above would have
+ * produced a message), so resolve them to OIDs and report
+ * them with format_type_be(). This keeps the wording
+ * consistent with the duplicate-object and undefined-object
+ * errors, which also use format_type_be().
+ */
+ Oid sourcetypeid = typenameTypeId(NULL, linitial_node(TypeName, castNode(List, object)));
+ Oid targettypeid = typenameTypeId(NULL, lsecond_node(TypeName, castNode(List, object)));
+
+ msg = gettext_noop("formatter for cast from type %s to type %s does not exist, skipping");
+ name = format_type_be(sourcetypeid);
+ args = format_type_be(targettypeid);
+ }
+ }
+ break;
case OBJECT_TRANSFORM:
if (!type_in_list_does_not_exist_skipping(list_make1(linitial(castNode(List, object))), &msg, &name))
{
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index adc6eabc0f4..139bdf51c61 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -2301,6 +2301,7 @@ stringify_grant_objtype(ObjectType objtype)
case OBJECT_AMPROC:
case OBJECT_ATTRIBUTE:
case OBJECT_CAST:
+ case OBJECT_FORMATTER:
case OBJECT_COLLATION:
case OBJECT_CONVERSION:
case OBJECT_DEFAULT:
@@ -2385,6 +2386,7 @@ stringify_adefprivs_objtype(ObjectType objtype)
case OBJECT_AMPROC:
case OBJECT_ATTRIBUTE:
case OBJECT_CAST:
+ case OBJECT_FORMATTER:
case OBJECT_COLLATION:
case OBJECT_CONVERSION:
case OBJECT_DEFAULT:
diff --git a/src/backend/commands/formattercmds.c b/src/backend/commands/formattercmds.c
new file mode 100644
index 00000000000..2276519f562
--- /dev/null
+++ b/src/backend/commands/formattercmds.c
@@ -0,0 +1,245 @@
+/*-------------------------------------------------------------------------
+ *
+ * formattercmds.c
+ * Routines for SQL commands that manipulate formatters.
+ *
+ * A formatter associates a function with a (source type, target type) pair.
+ * The function implements CAST(expr AS target FORMAT format_expr): it
+ * receives the source value and the FORMAT expression (passed as text) and
+ * returns the target type. Formatters are registered in the pg_formatter
+ * catalog and are intentionally separate from pg_cast (see pg_formatter.h).
+ *
+ * This file provides the catalog registration machinery (CREATE/DROP
+ * FORMATTER). It does not perform expression transformation or execution of
+ * formatted casts.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/commands/formattercmds.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "catalog/catalog.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/objectaccess.h"
+#include "catalog/objectaddress.h"
+#include "catalog/pg_formatter.h"
+#include "catalog/pg_proc.h"
+#include "catalog/pg_type.h"
+#include "commands/formatter.h"
+#include "miscadmin.h"
+#include "parser/parse_func.h"
+#include "parser/parse_type.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+/*
+ * Validate the signature of a formatter function: it must be a normal
+ * function (not a procedure, aggregate or window function), must not return
+ * a set, and must have the signature
+ *
+ * formatter(source_type, text) returns target_type
+ *
+ * NB: the formatter signature is fixed at two arguments and does not include
+ * the target type modifier; typmod-aware formatter signatures are not
+ * supported.
+ */
+static void
+check_formatter_function(Form_pg_proc procstruct,
+ Oid sourcetypeid, Oid targettypeid)
+{
+ if (procstruct->prokind != PROKIND_FUNCTION)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("formatter function must be a normal function")));
+ if (procstruct->proretset)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("formatter function must not return a set")));
+ if (procstruct->pronargs != 2)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("formatter function must take exactly two arguments")));
+ if (procstruct->proargtypes.values[0] != sourcetypeid)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("first argument of formatter function must be type %s",
+ format_type_be(sourcetypeid))));
+ if (procstruct->proargtypes.values[1] != TEXTOID)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("second argument of formatter function must be type %s",
+ "text")));
+ if (procstruct->prorettype != targettypeid)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("return data type of formatter function must be type %s",
+ format_type_be(targettypeid))));
+}
+
+/*
+ * CREATE FORMATTER
+ */
+ObjectAddress
+CreateFormatter(CreateFormatterStmt *stmt)
+{
+ Oid sourcetypeid;
+ Oid targettypeid;
+ char sourcetyptype;
+ char targettyptype;
+ Oid funcid;
+ AclResult aclresult;
+ Form_pg_proc procstruct;
+ HeapTuple tuple;
+ HeapTuple newtuple;
+ Datum values[Natts_pg_formatter];
+ bool nulls[Natts_pg_formatter] = {0};
+ Oid formatterid;
+ Relation relation;
+ ObjectAddress myself,
+ referenced;
+ ObjectAddresses *addrs;
+
+ sourcetypeid = typenameTypeId(NULL, stmt->sourcetype);
+ targettypeid = typenameTypeId(NULL, stmt->targettype);
+ sourcetyptype = get_typtype(sourcetypeid);
+ targettyptype = get_typtype(targettypeid);
+
+ /* No pseudo-types allowed */
+ if (sourcetyptype == TYPTYPE_PSEUDO)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("source data type %s is a pseudo-type",
+ TypeNameToString(stmt->sourcetype))));
+ if (targettyptype == TYPTYPE_PSEUDO)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("target data type %s is a pseudo-type",
+ TypeNameToString(stmt->targettype))));
+
+ /*
+ * Permission check. As for CREATE CAST, the caller must own at least one
+ * of the two types involved; owning a type is what authorizes defining
+ * conversion behavior for it. In addition, and following CREATE OPERATOR
+ * and CREATE AGGREGATE, we require EXECUTE permission on the formatter
+ * function (this will also be checked when the formatter is used, but it
+ * is a good idea to verify it up front). We intentionally do not require
+ * ownership of the function, unlike CREATE TRANSFORM, because a formatter
+ * is a data conversion rather than a procedural-language binding.
+ */
+ if (!object_ownercheck(TypeRelationId, sourcetypeid, GetUserId())
+ && !object_ownercheck(TypeRelationId, targettypeid, GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be owner of type %s or type %s",
+ format_type_be(sourcetypeid),
+ format_type_be(targettypeid))));
+
+ /*
+ * Look up and validate the formatter function.
+ */
+ funcid = LookupFuncWithArgs(OBJECT_FUNCTION, stmt->func, false);
+
+ aclresult = object_aclcheck(ProcedureRelationId, funcid, GetUserId(), ACL_EXECUTE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_FUNCTION,
+ NameListToString(stmt->func->objname));
+
+ tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for function %u", funcid);
+ procstruct = (Form_pg_proc) GETSTRUCT(tuple);
+ check_formatter_function(procstruct, sourcetypeid, targettypeid);
+ ReleaseSysCache(tuple);
+
+ /*
+ * Check for a pre-existing formatter for this (source, target) pair. For
+ * this version only one formatter per pair is allowed.
+ */
+ relation = table_open(FormatterRelationId, RowExclusiveLock);
+
+ if (SearchSysCacheExists2(FORMATTERSOURCETARGET,
+ ObjectIdGetDatum(sourcetypeid),
+ ObjectIdGetDatum(targettypeid)))
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_OBJECT),
+ errmsg("formatter for cast from type %s to type %s already exists",
+ format_type_be(sourcetypeid),
+ format_type_be(targettypeid))));
+
+ /*
+ * Build and insert the catalog tuple.
+ */
+ formatterid = GetNewOidWithIndex(relation, FormatterOidIndexId,
+ Anum_pg_formatter_oid);
+ values[Anum_pg_formatter_oid - 1] = ObjectIdGetDatum(formatterid);
+ values[Anum_pg_formatter_fmtsource - 1] = ObjectIdGetDatum(sourcetypeid);
+ values[Anum_pg_formatter_fmttarget - 1] = ObjectIdGetDatum(targettypeid);
+ values[Anum_pg_formatter_fmtfunc - 1] = ObjectIdGetDatum(funcid);
+
+ newtuple = heap_form_tuple(RelationGetDescr(relation), values, nulls);
+ CatalogTupleInsert(relation, newtuple);
+
+ addrs = new_object_addresses();
+
+ ObjectAddressSet(myself, FormatterRelationId, formatterid);
+
+ /* dependency on source type */
+ ObjectAddressSet(referenced, TypeRelationId, sourcetypeid);
+ add_exact_object_address(&referenced, addrs);
+
+ /* dependency on target type */
+ ObjectAddressSet(referenced, TypeRelationId, targettypeid);
+ add_exact_object_address(&referenced, addrs);
+
+ /* dependency on the formatter function */
+ ObjectAddressSet(referenced, ProcedureRelationId, funcid);
+ add_exact_object_address(&referenced, addrs);
+
+ record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL);
+ free_object_addresses(addrs);
+
+ /* dependency on extension */
+ recordDependencyOnCurrentExtension(&myself, false);
+
+ /* Post creation hook for new formatter */
+ InvokeObjectPostCreateHook(FormatterRelationId, formatterid, 0);
+
+ heap_freetuple(newtuple);
+
+ table_close(relation, RowExclusiveLock);
+
+ return myself;
+}
+
+/*
+ * get_formatter_oid - given source and target type OIDs, look up a
+ * formatter OID
+ */
+Oid
+get_formatter_oid(Oid sourcetypeid, Oid targettypeid, bool missing_ok)
+{
+ Oid oid;
+
+ oid = GetSysCacheOid2(FORMATTERSOURCETARGET, Anum_pg_formatter_oid,
+ ObjectIdGetDatum(sourcetypeid),
+ ObjectIdGetDatum(targettypeid));
+ if (!OidIsValid(oid) && !missing_ok)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("formatter for cast from type %s to type %s does not exist",
+ format_type_be(sourcetypeid),
+ format_type_be(targettypeid))));
+ return oid;
+}
diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build
index 9f258d566eb..8c4ecb83c3a 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -26,6 +26,7 @@ backend_sources += files(
'explain_state.c',
'extension.c',
'foreigncmds.c',
+ 'formattercmds.c',
'functioncmds.c',
'indexcmds.c',
'lockcmds.c',
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 77542d04200..f7d1779dad8 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -66,6 +66,7 @@ SecLabelSupportsObjectType(ObjectType objtype)
case OBJECT_AMPROC:
case OBJECT_ATTRIBUTE:
case OBJECT_CAST:
+ case OBJECT_FORMATTER:
case OBJECT_COLLATION:
case OBJECT_CONVERSION:
case OBJECT_DEFAULT:
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ef4881efc81..6e82a4e090e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -295,13 +295,14 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt
CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt
CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt
+ CreateFormatterStmt
CreatePropGraphStmt AlterPropGraphStmt
CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt
CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt
DropOpClassStmt DropOpFamilyStmt DropStmt
DropCastStmt DropRoleStmt
DropdbStmt DropTableSpaceStmt
- DropTransformStmt
+ DropTransformStmt DropFormatterStmt
DropUserMappingStmt ExplainStmt FetchStmt
GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt
ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt
@@ -769,7 +770,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
EXPLAIN EXPRESSION EXTENSION EXTERNAL EXTRACT
FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
- FORCE FOREIGN FORMAT FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS
+ FORCE FOREIGN FORMAT FORMATTER FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS
GENERATED GLOBAL GRANT GRANTED GRAPH GRAPH_TABLE GREATEST GROUP_P GROUPING GROUPS
@@ -1105,6 +1106,7 @@ stmt:
| CreateStatsStmt
| CreateTableSpaceStmt
| CreateTransformStmt
+ | CreateFormatterStmt
| CreateTrigStmt
| CreateEventTrigStmt
| CreateRoleStmt
@@ -1125,6 +1127,7 @@ stmt:
| DropSubscriptionStmt
| DropTableSpaceStmt
| DropTransformStmt
+ | DropFormatterStmt
| DropRoleStmt
| DropUserMappingStmt
| DropdbStmt
@@ -9893,6 +9896,38 @@ DropTransformStmt: DROP TRANSFORM opt_if_exists FOR Typename LANGUAGE name opt_d
;
+/*****************************************************************************
+ *
+ * CREATE FORMATTER / DROP FORMATTER
+ *
+ * A formatter registers the function implementing
+ * CAST(expr AS target FORMAT format_expr) for a (source, target) type pair.
+ *****************************************************************************/
+
+CreateFormatterStmt: CREATE FORMATTER FOR CAST '(' Typename AS Typename ')' WITH FUNCTION function_with_argtypes
+ {
+ CreateFormatterStmt *n = makeNode(CreateFormatterStmt);
+
+ n->sourcetype = $6;
+ n->targettype = $8;
+ n->func = $12;
+ $$ = (Node *) n;
+ }
+ ;
+
+DropFormatterStmt: DROP FORMATTER opt_if_exists FOR CAST '(' Typename AS Typename ')' opt_drop_behavior
+ {
+ DropStmt *n = makeNode(DropStmt);
+
+ n->removeType = OBJECT_FORMATTER;
+ n->objects = list_make1(list_make2($7, $9));
+ n->behavior = $11;
+ n->missing_ok = $3;
+ $$ = (Node *) n;
+ }
+ ;
+
+
/*****************************************************************************
*
* QUERY:
@@ -18933,6 +18968,7 @@ unreserved_keyword:
| FOLLOWING
| FORCE
| FORMAT
+ | FORMATTER
| FORWARD
| FUNCTION
| FUNCTIONS
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..ee66296e0d0 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -37,6 +37,7 @@
#include "commands/event_trigger.h"
#include "commands/explain.h"
#include "commands/extension.h"
+#include "commands/formatter.h"
#include "commands/lockcmds.h"
#include "commands/matview.h"
#include "commands/policy.h"
@@ -193,6 +194,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
case T_CreateTableAsStmt:
case T_CreateTableSpaceStmt:
case T_CreateTransformStmt:
+ case T_CreateFormatterStmt:
case T_CreateTrigStmt:
case T_CreateUserMappingStmt:
case T_CreatedbStmt:
@@ -1755,6 +1757,10 @@ ProcessUtilitySlow(ParseState *pstate,
address = CreateTransform((CreateTransformStmt *) parsetree);
break;
+ case T_CreateFormatterStmt:
+ address = CreateFormatter((CreateFormatterStmt *) parsetree);
+ break;
+
case T_AlterOpFamilyStmt:
AlterOpFamily((AlterOpFamilyStmt *) parsetree);
/* commands are stashed in AlterOpFamily */
@@ -2666,6 +2672,9 @@ CreateCommandTag(Node *parsetree)
case OBJECT_TRANSFORM:
tag = CMDTAG_DROP_TRANSFORM;
break;
+ case OBJECT_FORMATTER:
+ tag = CMDTAG_DROP_FORMATTER;
+ break;
case OBJECT_ACCESS_METHOD:
tag = CMDTAG_DROP_ACCESS_METHOD;
break;
@@ -2981,6 +2990,10 @@ CreateCommandTag(Node *parsetree)
tag = CMDTAG_CREATE_TRANSFORM;
break;
+ case T_CreateFormatterStmt:
+ tag = CMDTAG_CREATE_FORMATTER;
+ break;
+
case T_CreateTrigStmt:
tag = CMDTAG_CREATE_TRIGGER;
break;
@@ -3690,6 +3703,10 @@ GetCommandLogLevel(Node *parsetree)
lev = LOGSTMT_DDL;
break;
+ case T_CreateFormatterStmt:
+ lev = LOGSTMT_DDL;
+ break;
+
case T_AlterOpFamilyStmt:
lev = LOGSTMT_DDL;
break;
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index dc98c5c5c09..d70753905eb 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -188,6 +188,9 @@ getSchemaData(Archive *fout, int *numTablesPtr)
pg_log_info("reading transforms");
getTransforms(fout);
+ pg_log_info("reading formatters");
+ getFormatters(fout);
+
pg_log_info("reading table inheritance information");
inhinfo = getInherits(fout, &numInherits);
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c56437d6057..92883740743 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -300,6 +300,7 @@ static void dumpProcLang(Archive *fout, const ProcLangInfo *plang);
static void dumpFunc(Archive *fout, const FuncInfo *finfo);
static void dumpCast(Archive *fout, const CastInfo *cast);
static void dumpTransform(Archive *fout, const TransformInfo *transform);
+static void dumpFormatter(Archive *fout, const FormatterInfo *formatter);
static void dumpOpr(Archive *fout, const OprInfo *oprinfo);
static void dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo);
static void dumpOpclass(Archive *fout, const OpclassInfo *opcinfo);
@@ -9320,6 +9321,83 @@ getTransforms(Archive *fout)
destroyPQExpBuffer(query);
}
+/*
+ * getFormatters
+ * get basic information about every formatter in the system
+ */
+void
+getFormatters(Archive *fout)
+{
+ PGresult *res;
+ int ntups;
+ int i;
+ PQExpBuffer query;
+ FormatterInfo *formatterinfo;
+ int i_tableoid;
+ int i_oid;
+ int i_fmtsource;
+ int i_fmttarget;
+ int i_fmtfunc;
+
+ /* Formatters were introduced in v19 */
+ if (fout->remoteVersion < 190000)
+ return;
+
+ query = createPQExpBuffer();
+
+ appendPQExpBufferStr(query, "SELECT tableoid, oid, "
+ "fmtsource, fmttarget, fmtfunc "
+ "FROM pg_formatter "
+ "ORDER BY 3,4");
+
+ res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+ ntups = PQntuples(res);
+
+ formatterinfo = pg_malloc_array(FormatterInfo, ntups);
+
+ i_tableoid = PQfnumber(res, "tableoid");
+ i_oid = PQfnumber(res, "oid");
+ i_fmtsource = PQfnumber(res, "fmtsource");
+ i_fmttarget = PQfnumber(res, "fmttarget");
+ i_fmtfunc = PQfnumber(res, "fmtfunc");
+
+ for (i = 0; i < ntups; i++)
+ {
+ PQExpBufferData namebuf;
+ TypeInfo *sTypeInfo;
+ TypeInfo *tTypeInfo;
+
+ formatterinfo[i].dobj.objType = DO_FORMATTER;
+ formatterinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+ formatterinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+ AssignDumpId(&formatterinfo[i].dobj);
+ formatterinfo[i].fmtsource = atooid(PQgetvalue(res, i, i_fmtsource));
+ formatterinfo[i].fmttarget = atooid(PQgetvalue(res, i, i_fmttarget));
+ formatterinfo[i].fmtfunc = atooid(PQgetvalue(res, i, i_fmtfunc));
+
+ /*
+ * Try to name the formatter as a concatenation of the type names.
+ * This is only used for purposes of sorting. If we fail to find
+ * either type, the name will be an empty string.
+ */
+ initPQExpBuffer(&namebuf);
+ sTypeInfo = findTypeByOid(formatterinfo[i].fmtsource);
+ tTypeInfo = findTypeByOid(formatterinfo[i].fmttarget);
+ if (sTypeInfo && tTypeInfo)
+ appendPQExpBuffer(&namebuf, "%s %s",
+ sTypeInfo->dobj.name, tTypeInfo->dobj.name);
+ formatterinfo[i].dobj.name = namebuf.data;
+
+ /* Decide whether we want to dump it */
+ selectDumpableObject(&(formatterinfo[i].dobj), fout);
+ }
+
+ PQclear(res);
+
+ destroyPQExpBuffer(query);
+}
+
/*
* getTableAttrs -
* for each interesting table, read info about its attributes
@@ -11913,6 +11991,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
case DO_TRANSFORM:
dumpTransform(fout, (const TransformInfo *) dobj);
break;
+ case DO_FORMATTER:
+ dumpFormatter(fout, (const FormatterInfo *) dobj);
+ break;
case DO_SEQUENCE_SET:
dumpSequenceData(fout, (const TableDataInfo *) dobj);
break;
@@ -14255,6 +14336,90 @@ dumpTransform(Archive *fout, const TransformInfo *transform)
destroyPQExpBuffer(transformargs);
}
+/*
+ * Dump a formatter
+ */
+static void
+dumpFormatter(Archive *fout, const FormatterInfo *formatter)
+{
+ DumpOptions *dopt = fout->dopt;
+ PQExpBuffer defqry;
+ PQExpBuffer delqry;
+ PQExpBuffer labelq;
+ PQExpBuffer formatterargs;
+ FuncInfo *funcInfo;
+ const char *sourceType;
+ const char *targetType;
+
+ /* Do nothing if not dumping schema */
+ if (!dopt->dumpSchema)
+ return;
+
+ /* Cannot dump if we don't have the formatter function's info */
+ funcInfo = findFuncByOid(formatter->fmtfunc);
+ if (funcInfo == NULL)
+ pg_fatal("could not find function definition for function with OID %u",
+ formatter->fmtfunc);
+
+ defqry = createPQExpBuffer();
+ delqry = createPQExpBuffer();
+ labelq = createPQExpBuffer();
+ formatterargs = createPQExpBuffer();
+
+ sourceType = getFormattedTypeName(fout, formatter->fmtsource, zeroAsNone);
+ targetType = getFormattedTypeName(fout, formatter->fmttarget, zeroAsNone);
+
+ appendPQExpBuffer(delqry, "DROP FORMATTER FOR CAST (%s AS %s);\n",
+ sourceType, targetType);
+
+ appendPQExpBuffer(defqry, "CREATE FORMATTER FOR CAST (%s AS %s) ",
+ sourceType, targetType);
+
+ {
+ char *fsig = format_function_signature(fout, funcInfo, true);
+
+ /*
+ * Always qualify the function name (format_function_signature won't
+ * qualify it).
+ */
+ appendPQExpBuffer(defqry, "WITH FUNCTION %s.%s",
+ fmtId(funcInfo->dobj.namespace->dobj.name), fsig);
+ free(fsig);
+ }
+ appendPQExpBufferStr(defqry, ";\n");
+
+ appendPQExpBuffer(labelq, "FORMATTER FOR CAST (%s AS %s)",
+ sourceType, targetType);
+
+ appendPQExpBuffer(formatterargs, "FOR CAST (%s AS %s)",
+ sourceType, targetType);
+
+ if (dopt->binary_upgrade)
+ binary_upgrade_extension_member(defqry, &formatter->dobj,
+ "FORMATTER", formatterargs->data, NULL);
+
+ if (formatter->dobj.dump & DUMP_COMPONENT_DEFINITION)
+ ArchiveEntry(fout, formatter->dobj.catId, formatter->dobj.dumpId,
+ ARCHIVE_OPTS(.tag = labelq->data,
+ .description = "FORMATTER",
+ .section = SECTION_PRE_DATA,
+ .createStmt = defqry->data,
+ .dropStmt = delqry->data,
+ .deps = formatter->dobj.dependencies,
+ .nDeps = formatter->dobj.nDeps));
+
+ /* Dump Formatter Comments */
+ if (formatter->dobj.dump & DUMP_COMPONENT_COMMENT)
+ dumpComment(fout, "FORMATTER", formatterargs->data,
+ NULL, "",
+ formatter->dobj.catId, 0, formatter->dobj.dumpId);
+
+ destroyPQExpBuffer(defqry);
+ destroyPQExpBuffer(delqry);
+ destroyPQExpBuffer(labelq);
+ destroyPQExpBuffer(formatterargs);
+}
+
/*
* dumpOpr
@@ -20696,6 +20861,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
case DO_FDW:
case DO_FOREIGN_SERVER:
case DO_TRANSFORM:
+ case DO_FORMATTER:
/* Pre-data objects: must come before the pre-data boundary */
addObjectDependency(preDataBound, dobj->dumpId);
break;
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 5a6726d8b12..b3adb97b5ba 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -73,6 +73,7 @@ typedef enum
DO_FOREIGN_SERVER,
DO_DEFAULT_ACL,
DO_TRANSFORM,
+ DO_FORMATTER,
DO_LARGE_OBJECT,
DO_LARGE_OBJECT_DATA,
DO_PRE_DATA_BOUNDARY,
@@ -559,6 +560,14 @@ typedef struct _transformInfo
Oid trftosql;
} TransformInfo;
+typedef struct _formatterInfo
+{
+ DumpableObject dobj;
+ Oid fmtsource;
+ Oid fmttarget;
+ Oid fmtfunc;
+} FormatterInfo;
+
/* InhInfo isn't a DumpableObject, just temporary state */
typedef struct _inhInfo
{
@@ -814,6 +823,7 @@ extern void getTriggers(Archive *fout, TableInfo tblinfo[], int numTables);
extern void getProcLangs(Archive *fout);
extern void getCasts(Archive *fout);
extern void getTransforms(Archive *fout);
+extern void getFormatters(Archive *fout);
extern void getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables);
extern bool shouldPrintColumn(const DumpOptions *dopt, const TableInfo *tbinfo, int colno);
extern void getTSParsers(Archive *fout);
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 03e5c1c1116..65df81c9324 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -60,6 +60,7 @@ enum dbObjectTypePriorities
PRIO_EXTENSION,
PRIO_TYPE, /* used for DO_TYPE and DO_SHELL_TYPE */
PRIO_CAST,
+ PRIO_FORMATTER,
PRIO_FUNC,
PRIO_AGG,
PRIO_ACCESS_METHOD,
@@ -128,6 +129,7 @@ static const int dbObjectTypePriority[] =
[DO_FK_CONSTRAINT] = PRIO_FK_CONSTRAINT,
[DO_PROCLANG] = PRIO_PROCLANG,
[DO_CAST] = PRIO_CAST,
+ [DO_FORMATTER] = PRIO_FORMATTER,
[DO_TABLE_DATA] = PRIO_TABLE_DATA,
[DO_SEQUENCE_SET] = PRIO_SEQUENCE_SET,
[DO_DUMMY_TYPE] = PRIO_DUMMY_TYPE,
@@ -1649,6 +1651,13 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
((CastInfo *) obj)->casttarget,
obj->dumpId, obj->catId.oid);
return;
+ case DO_FORMATTER:
+ snprintf(buf, bufsize,
+ "FORMATTER %u to %u (ID %d OID %u)",
+ ((FormatterInfo *) obj)->fmtsource,
+ ((FormatterInfo *) obj)->fmttarget,
+ obj->dumpId, obj->catId.oid);
+ return;
case DO_TRANSFORM:
snprintf(buf, bufsize,
"TRANSFORM %u lang %u (ID %d OID %u)",
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index bab57372b88..872f22b3910 100644
--- a/src/include/catalog/Makefile
+++ b/src/include/catalog/Makefile
@@ -75,6 +75,7 @@ CATALOG_HEADERS := \
pg_parameter_acl.h \
pg_partitioned_table.h \
pg_range.h \
+ pg_formatter.h \
pg_transform.h \
pg_sequence.h \
pg_publication.h \
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 635c0d9cb13..6402cc76c87 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
*/
/* yyyymmddN */
-#define CATALOG_VERSION_NO 202606281
+#define CATALOG_VERSION_NO 202606282
#endif
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index fa836e4ee25..ea7fef5744d 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -62,6 +62,7 @@ catalog_headers = [
'pg_parameter_acl.h',
'pg_partitioned_table.h',
'pg_range.h',
+ 'pg_formatter.h',
'pg_transform.h',
'pg_sequence.h',
'pg_publication.h',
diff --git a/src/include/catalog/pg_formatter.h b/src/include/catalog/pg_formatter.h
new file mode 100644
index 00000000000..e41a1de54ce
--- /dev/null
+++ b/src/include/catalog/pg_formatter.h
@@ -0,0 +1,62 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_formatter.h
+ * definition of the "formatter" system catalog (pg_formatter)
+ *
+ * A formatter registers a function that implements
+ * CAST(expr AS target FORMAT format_expr) for a particular
+ * (source type, target type) pair. The formatter function receives the
+ * source value and the FORMAT expression (as text) and returns the target
+ * type. This is intentionally separate from pg_cast: ordinary casts have
+ * implicit/assignment/explicit contexts and binary-coercible/WITH INOUT/
+ * WITHOUT FUNCTION methods, whereas a formatted cast is always explicit and
+ * always requires a function.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_formatter.h
+ *
+ * NOTES
+ * The Catalog.pm module reads this file and derives schema
+ * information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_FORMATTER_H
+#define PG_FORMATTER_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_formatter_d.h" /* IWYU pragma: export */
+
+/* ----------------
+ * pg_formatter definition. cpp turns this into
+ * typedef struct FormData_pg_formatter
+ * ----------------
+ */
+BEGIN_CATALOG_STRUCT
+
+CATALOG(pg_formatter,8647,FormatterRelationId)
+{
+ Oid oid; /* oid */
+ Oid fmtsource BKI_LOOKUP(pg_type); /* source type */
+ Oid fmttarget BKI_LOOKUP(pg_type); /* target type */
+ Oid fmtfunc BKI_LOOKUP(pg_proc); /* formatter function */
+} FormData_pg_formatter;
+
+END_CATALOG_STRUCT
+
+/* ----------------
+ * Form_pg_formatter corresponds to a pointer to a tuple with
+ * the format of pg_formatter relation.
+ * ----------------
+ */
+typedef FormData_pg_formatter *Form_pg_formatter;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_formatter_oid_index, 8648, FormatterOidIndexId, pg_formatter, btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_formatter_source_target_index, 8649, FormatterSourceTargetIndexId, pg_formatter, btree(fmtsource oid_ops, fmttarget oid_ops));
+
+MAKE_SYSCACHE(FORMATTEROID, pg_formatter_oid_index, 8);
+MAKE_SYSCACHE(FORMATTERSOURCETARGET, pg_formatter_source_target_index, 8);
+
+#endif /* PG_FORMATTER_H */
diff --git a/src/include/commands/formatter.h b/src/include/commands/formatter.h
new file mode 100644
index 00000000000..c68a8745670
--- /dev/null
+++ b/src/include/commands/formatter.h
@@ -0,0 +1,24 @@
+/*-------------------------------------------------------------------------
+ *
+ * formatter.h
+ * prototypes for formattercmds.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/formatter.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef FORMATTER_H
+#define FORMATTER_H
+
+#include "catalog/objectaddress.h"
+#include "nodes/parsenodes.h"
+
+extern ObjectAddress CreateFormatter(CreateFormatterStmt *stmt);
+extern Oid get_formatter_oid(Oid sourcetypeid, Oid targettypeid,
+ bool missing_ok);
+
+#endif /* FORMATTER_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 759c6bfae54..0b0431f4b6a 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2445,6 +2445,7 @@ typedef enum ObjectType
OBJECT_FDW,
OBJECT_FOREIGN_SERVER,
OBJECT_FOREIGN_TABLE,
+ OBJECT_FORMATTER,
OBJECT_FUNCTION,
OBJECT_INDEX,
OBJECT_LANGUAGE,
@@ -4356,6 +4357,22 @@ typedef struct CreateTransformStmt
ObjectWithArgs *tosql;
} CreateTransformStmt;
+/* ----------------------
+ * CREATE FORMATTER Statement
+ *
+ * Registers a formatter function for CAST(... AS target FORMAT ...) on a
+ * (sourcetype, targettype) pair. DROP FORMATTER reuses the generic
+ * DropStmt path with removeType == OBJECT_FORMATTER.
+ * ----------------------
+ */
+typedef struct CreateFormatterStmt
+{
+ NodeTag type;
+ TypeName *sourcetype; /* source data type */
+ TypeName *targettype; /* target data type */
+ ObjectWithArgs *func; /* formatter function */
+} CreateFormatterStmt;
+
/* ----------------------
* PREPARE Statement
* ----------------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 51ead54f015..7ce539c79f9 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -183,6 +183,7 @@ PG_KEYWORD("for", FOR, RESERVED_KEYWORD, AS_LABEL)
PG_KEYWORD("force", FORCE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("foreign", FOREIGN, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("format", FORMAT, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("formatter", FORMATTER, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("forward", FORWARD, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("freeze", FREEZE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("from", FROM, RESERVED_KEYWORD, AS_LABEL)
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index befae5f6b4f..c5701b4a8aa 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -95,6 +95,7 @@ PG_CMDTAG(CMDTAG_CREATE_EVENT_TRIGGER, "CREATE EVENT TRIGGER", false, false, fal
PG_CMDTAG(CMDTAG_CREATE_EXTENSION, "CREATE EXTENSION", true, false, false)
PG_CMDTAG(CMDTAG_CREATE_FOREIGN_DATA_WRAPPER, "CREATE FOREIGN DATA WRAPPER", true, false, false)
PG_CMDTAG(CMDTAG_CREATE_FOREIGN_TABLE, "CREATE FOREIGN TABLE", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_FORMATTER, "CREATE FORMATTER", true, false, false)
PG_CMDTAG(CMDTAG_CREATE_FUNCTION, "CREATE FUNCTION", true, false, false)
PG_CMDTAG(CMDTAG_CREATE_INDEX, "CREATE INDEX", true, false, false)
PG_CMDTAG(CMDTAG_CREATE_LANGUAGE, "CREATE LANGUAGE", true, false, false)
@@ -148,6 +149,7 @@ PG_CMDTAG(CMDTAG_DROP_EVENT_TRIGGER, "DROP EVENT TRIGGER", false, false, false)
PG_CMDTAG(CMDTAG_DROP_EXTENSION, "DROP EXTENSION", true, false, false)
PG_CMDTAG(CMDTAG_DROP_FOREIGN_DATA_WRAPPER, "DROP FOREIGN DATA WRAPPER", true, false, false)
PG_CMDTAG(CMDTAG_DROP_FOREIGN_TABLE, "DROP FOREIGN TABLE", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_FORMATTER, "DROP FORMATTER", true, false, false)
PG_CMDTAG(CMDTAG_DROP_FUNCTION, "DROP FUNCTION", true, false, false)
PG_CMDTAG(CMDTAG_DROP_INDEX, "DROP INDEX", true, false, false)
PG_CMDTAG(CMDTAG_DROP_LANGUAGE, "DROP LANGUAGE", true, false, false)
diff --git a/src/test/regress/expected/formatters.out b/src/test/regress/expected/formatters.out
new file mode 100644
index 00000000000..bb1f091a1b8
--- /dev/null
+++ b/src/test/regress/expected/formatters.out
@@ -0,0 +1,116 @@
+--
+-- FORMATTERS
+--
+-- CREATE/DROP FORMATTER registers formatter metadata in pg_formatter,
+-- keyed by a (source type, target type) pair. This is catalog and DDL
+-- infrastructure only; it does not transform or execute formatted casts.
+-- A simple formatter function with the required signature
+-- formatter(source_type, text) returns target_type
+CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2;
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION int4_to_text_fmt(integer, text);
+-- It shows up in the catalog (use regtype/regprocedure, not raw OIDs)
+SELECT fmtsource::regtype, fmttarget::regtype, fmtfunc::regprocedure
+ FROM pg_formatter
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype;
+ fmtsource | fmttarget | fmtfunc
+-----------+-----------+--------------------------------
+ integer | text | int4_to_text_fmt(integer,text)
+(1 row)
+
+-- ... and as a first-class object
+SELECT pg_describe_object('pg_formatter'::regclass, oid, 0)
+ FROM pg_formatter
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype;
+ pg_describe_object
+-----------------------------------------
+ formatter for cast from integer to text
+(1 row)
+
+-- Only one formatter per (source, target) pair
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION int4_to_text_fmt(integer, text);
+ERROR: formatter for cast from type integer to type text already exists
+-- Function signature validation
+CREATE FUNCTION fmt_bad_nargs(integer) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text;
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION fmt_bad_nargs(integer); -- wrong # of arguments
+ERROR: formatter function must take exactly two arguments
+CREATE FUNCTION fmt_bad_arg2(integer, integer) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text;
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION fmt_bad_arg2(integer, integer); -- second arg not text
+ERROR: second argument of formatter function must be type text
+CREATE FUNCTION fmt_bad_arg1(bigint, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text;
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION fmt_bad_arg1(bigint, text); -- first arg mismatch
+ERROR: first argument of formatter function must be type integer
+CREATE FUNCTION fmt_bad_ret(integer, text) RETURNS boolean
+ LANGUAGE sql IMMUTABLE RETURN true;
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION fmt_bad_ret(integer, text); -- return type mismatch
+ERROR: return data type of formatter function must be type text
+CREATE FUNCTION fmt_bad_set(integer, text) RETURNS SETOF text
+ LANGUAGE sql IMMUTABLE AS $$ SELECT $1::text $$;
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION fmt_bad_set(integer, text); -- set-returning rejected
+ERROR: formatter function must not return a set
+-- No pseudo-types
+CREATE FUNCTION fmt_anyel(anyelement, text) RETURNS text
+ LANGUAGE sql IMMUTABLE AS $$ SELECT $2 $$;
+CREATE FORMATTER FOR CAST (anyelement AS text)
+ WITH FUNCTION fmt_anyel(anyelement, text);
+ERROR: source data type anyelement is a pseudo-type
+-- Registering a formatter does not enable a formatted cast: the FORMAT
+-- clause must not be silently ignored or rewritten to a built-in function,
+-- so CAST(... FORMAT ...) is rejected during parse analysis.
+SELECT CAST(5 AS text FORMAT 'YYYY');
+ERROR: formatted casts are not implemented yet
+LINE 1: SELECT CAST(5 AS text FORMAT 'YYYY');
+ ^
+DETAIL: No formatter resolution mechanism is available.
+-- Dependency behavior: the formatter depends on its function.
+DROP FUNCTION int4_to_text_fmt(integer, text); -- fails (RESTRICT)
+ERROR: cannot drop function int4_to_text_fmt(integer,text) because other objects depend on it
+DETAIL: formatter for cast from integer to text depends on function int4_to_text_fmt(integer,text)
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+DROP FUNCTION int4_to_text_fmt(integer, text) CASCADE; -- drops formatter
+NOTICE: drop cascades to formatter for cast from integer to text
+SELECT count(*) FROM pg_formatter
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype;
+ count
+-------
+ 0
+(1 row)
+
+-- Privileges: the creator must own the source or the target type. (The
+-- ownership check happens before the function is looked up, so the function
+-- name here is irrelevant.)
+CREATE ROLE regress_formatter_user;
+SET ROLE regress_formatter_user;
+CREATE FORMATTER FOR CAST (text AS integer)
+ WITH FUNCTION pg_catalog.length(text);
+ERROR: must be owner of type text or type integer
+RESET ROLE;
+DROP ROLE regress_formatter_user;
+-- DROP FORMATTER, including IF EXISTS
+CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2;
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION int4_to_text_fmt(integer, text);
+DROP FORMATTER FOR CAST (integer AS text);
+DROP FORMATTER FOR CAST (integer AS text); -- fails, gone
+ERROR: formatter for cast from type integer to type text does not exist
+DROP FORMATTER IF EXISTS FOR CAST (integer AS text); -- notice, no error
+NOTICE: formatter for cast from type integer to type text does not exist, skipping
+-- Clean up
+DROP FUNCTION int4_to_text_fmt(integer, text);
+DROP FUNCTION fmt_bad_nargs(integer);
+DROP FUNCTION fmt_bad_arg2(integer, integer);
+DROP FUNCTION fmt_bad_arg1(bigint, text);
+DROP FUNCTION fmt_bad_ret(integer, text);
+DROP FUNCTION fmt_bad_set(integer, text);
+DROP FUNCTION fmt_anyel(anyelement, text);
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index d64169b7bf0..6b60b02aab3 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -257,6 +257,9 @@ NOTICE: checking pg_range {rngmltconstruct1} => pg_proc {oid}
NOTICE: checking pg_range {rngmltconstruct2} => pg_proc {oid}
NOTICE: checking pg_range {rngcanonical} => pg_proc {oid}
NOTICE: checking pg_range {rngsubdiff} => pg_proc {oid}
+NOTICE: checking pg_formatter {fmtsource} => pg_type {oid}
+NOTICE: checking pg_formatter {fmttarget} => pg_type {oid}
+NOTICE: checking pg_formatter {fmtfunc} => pg_proc {oid}
NOTICE: checking pg_transform {trftype} => pg_type {oid}
NOTICE: checking pg_transform {trflang} => pg_language {oid}
NOTICE: checking pg_transform {trffromsql} => pg_proc {oid}
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..a0281f1fb2a 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi
# ----------
# Another group of parallel tests
# ----------
-test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse create_property_graph for_portion_of
+test: create_aggregate create_function_sql create_cast formatters constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse create_property_graph for_portion_of
# ----------
# sanity_check does a vacuum, affecting the sort order of SELECT *
diff --git a/src/test/regress/sql/formatters.sql b/src/test/regress/sql/formatters.sql
new file mode 100644
index 00000000000..8050aee6f45
--- /dev/null
+++ b/src/test/regress/sql/formatters.sql
@@ -0,0 +1,99 @@
+--
+-- FORMATTERS
+--
+-- CREATE/DROP FORMATTER registers formatter metadata in pg_formatter,
+-- keyed by a (source type, target type) pair. This is catalog and DDL
+-- infrastructure only; it does not transform or execute formatted casts.
+
+-- A simple formatter function with the required signature
+-- formatter(source_type, text) returns target_type
+CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2;
+
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION int4_to_text_fmt(integer, text);
+
+-- It shows up in the catalog (use regtype/regprocedure, not raw OIDs)
+SELECT fmtsource::regtype, fmttarget::regtype, fmtfunc::regprocedure
+ FROM pg_formatter
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype;
+
+-- ... and as a first-class object
+SELECT pg_describe_object('pg_formatter'::regclass, oid, 0)
+ FROM pg_formatter
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype;
+
+-- Only one formatter per (source, target) pair
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION int4_to_text_fmt(integer, text);
+
+-- Function signature validation
+CREATE FUNCTION fmt_bad_nargs(integer) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text;
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION fmt_bad_nargs(integer); -- wrong # of arguments
+
+CREATE FUNCTION fmt_bad_arg2(integer, integer) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text;
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION fmt_bad_arg2(integer, integer); -- second arg not text
+
+CREATE FUNCTION fmt_bad_arg1(bigint, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text;
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION fmt_bad_arg1(bigint, text); -- first arg mismatch
+
+CREATE FUNCTION fmt_bad_ret(integer, text) RETURNS boolean
+ LANGUAGE sql IMMUTABLE RETURN true;
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION fmt_bad_ret(integer, text); -- return type mismatch
+
+CREATE FUNCTION fmt_bad_set(integer, text) RETURNS SETOF text
+ LANGUAGE sql IMMUTABLE AS $$ SELECT $1::text $$;
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION fmt_bad_set(integer, text); -- set-returning rejected
+
+-- No pseudo-types
+CREATE FUNCTION fmt_anyel(anyelement, text) RETURNS text
+ LANGUAGE sql IMMUTABLE AS $$ SELECT $2 $$;
+CREATE FORMATTER FOR CAST (anyelement AS text)
+ WITH FUNCTION fmt_anyel(anyelement, text);
+
+-- Registering a formatter does not enable a formatted cast: the FORMAT
+-- clause must not be silently ignored or rewritten to a built-in function,
+-- so CAST(... FORMAT ...) is rejected during parse analysis.
+SELECT CAST(5 AS text FORMAT 'YYYY');
+
+-- Dependency behavior: the formatter depends on its function.
+DROP FUNCTION int4_to_text_fmt(integer, text); -- fails (RESTRICT)
+DROP FUNCTION int4_to_text_fmt(integer, text) CASCADE; -- drops formatter
+SELECT count(*) FROM pg_formatter
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype;
+
+-- Privileges: the creator must own the source or the target type. (The
+-- ownership check happens before the function is looked up, so the function
+-- name here is irrelevant.)
+CREATE ROLE regress_formatter_user;
+SET ROLE regress_formatter_user;
+CREATE FORMATTER FOR CAST (text AS integer)
+ WITH FUNCTION pg_catalog.length(text);
+RESET ROLE;
+DROP ROLE regress_formatter_user;
+
+-- DROP FORMATTER, including IF EXISTS
+CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2;
+CREATE FORMATTER FOR CAST (integer AS text)
+ WITH FUNCTION int4_to_text_fmt(integer, text);
+DROP FORMATTER FOR CAST (integer AS text);
+DROP FORMATTER FOR CAST (integer AS text); -- fails, gone
+DROP FORMATTER IF EXISTS FOR CAST (integer AS text); -- notice, no error
+
+-- Clean up
+DROP FUNCTION int4_to_text_fmt(integer, text);
+DROP FUNCTION fmt_bad_nargs(integer);
+DROP FUNCTION fmt_bad_arg2(integer, integer);
+DROP FUNCTION fmt_bad_arg1(bigint, text);
+DROP FUNCTION fmt_bad_ret(integer, text);
+DROP FUNCTION fmt_bad_set(integer, text);
+DROP FUNCTION fmt_anyel(anyelement, text);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index c5db6ca6705..2feea74b3a5 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -575,6 +575,7 @@ CreateExtensionStmt
CreateFdwStmt
CreateForeignServerStmt
CreateForeignTableStmt
+CreateFormatterStmt
CreateFunctionStmt
CreateOpClassItem
CreateOpClassStmt
@@ -921,6 +922,7 @@ FormData_pg_extension
FormData_pg_foreign_data_wrapper
FormData_pg_foreign_server
FormData_pg_foreign_table
+FormData_pg_formatter
FormData_pg_index
FormData_pg_inherits
FormData_pg_language
@@ -986,6 +988,7 @@ Form_pg_extension
Form_pg_foreign_data_wrapper
Form_pg_foreign_server
Form_pg_foreign_table
+Form_pg_formatter
Form_pg_index
Form_pg_inherits
Form_pg_language
@@ -1028,6 +1031,7 @@ Form_pg_ts_template
Form_pg_type
Form_pg_user_mapping
FormatNode
+FormatterInfo
FreeBlockNumberArray
FreeListData
FreePageBtree
--
2.54.0
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-06-29 08:47 jian he <[email protected]>
parent: Robert Haas <[email protected]>
2 siblings, 1 reply; 53+ messages in thread
From: jian he @ 2026-06-29 08:47 UTC (permalink / raw)
To: Robert Haas <[email protected]>; +Cc: Corey Huinker <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; pgsql-hackers
On Thu, Jun 18, 2026 at 10:52 PM Robert Haas <[email protected]> wrote:
>
> On Tue, Mar 31, 2026 at 1:48 PM Corey Huinker <[email protected]> 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.
>
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/
Attachments:
[text/x-patch] v9-0002-coerceUnknownConst-delicated-function-for-coerce-const.patch (11.6K, ../../CACJufxGVuCM4XFGqaqiV-VOEiqMtCZ3+T-+SrG-y6kqdLo1ZqA@mail.gmail.com/2-v9-0002-coerceUnknownConst-delicated-function-for-coerce-const.patch)
download | inline diff:
From ea0f465db370cf05901a37606d69a656c4e0f1e6 Mon Sep 17 00:00:00 2001
From: jian he <[email protected]>
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
[text/x-patch] v9-0001-Add-typformatin-and-typformatout-to-pg_type.patch (33.8K, ../../CACJufxGVuCM4XFGqaqiV-VOEiqMtCZ3+T-+SrG-y6kqdLo1ZqA@mail.gmail.com/3-v9-0001-Add-typformatin-and-typformatout-to-pg_type.patch)
download | inline diff:
From c0e3982a3d713ccacdd2cbeb84de48eab5e774a0 Mon Sep 17 00:00:00 2001
From: jian he <[email protected]>
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=NoLH_p6uuyesi6A@mail.gmail.com
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
[text/x-patch] v9-0003-CAST-expr-AS-type-FORMAT-template.patch (82.9K, ../../CACJufxGVuCM4XFGqaqiV-VOEiqMtCZ3+T-+SrG-y6kqdLo1ZqA@mail.gmail.com/4-v9-0003-CAST-expr-AS-type-FORMAT-template.patch)
download | inline diff:
From 3b316fe48bd8a5c1edeb7160f03a97b2bc8da11a Mon Sep 17 00:00:00 2001
From: jian he <[email protected]>
Date: Mon, 29 Jun 2026 15:35:56 +0800
Subject: [PATCH v9 3/3] 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=x2rCwa+ZmcgopDwGKDXha09qTfTpCj3QSTG6Y9Q@mail.gmail.com
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=NoLH_p6uuyesi6A@mail.gmail.com
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
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ff4e1388c55..ce1017bddfe 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);
@@ -16787,6 +16790,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"),
@@ -19943,12 +19948,26 @@ makeColumnRef(char *colname, List *indirection,
return (Node *) c;
}
+static Node *
+makeFormattedTypeCast(Node *arg, TypeName *typename, Node *format, int location)
+{
+ TypeCast *n = makeNode(TypeCast);
+
+ n->arg = arg;
+ n->typeName = typename;
+ n->format = format;
+ 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_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)
+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
+------------------------------
+ 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
------------------------------
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"
+LINE 1: SELECT cast('2011-12-18 11:38 JUNK' as timestamptz format 'Y...
+ ^
+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"
+LINE 1: SELECT cast('2011-12-18 11:38 ...' as timestamptz format 'YY...
+ ^
+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"
+LINE 1: SELECT cast('2011-12-18 11:38 +xyz' as timestamptz format 'Y...
+ ^
+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"
+LINE 1: SELECT cast('2011-12-18 11:38 +01:xyz' as timestamptz format...
+ ^
+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 +3549,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 +3568,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
--
@@ -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)
+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)
--
@@ -3868,6 +3980,27 @@ 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'),
+ 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)
+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
+-------------------------
+ 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
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..b6ef7ccecb2 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..e991016f828
--- /dev/null
+++ b/src/test/regress/sql/cast.sql
@@ -0,0 +1,123 @@
+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('hello' as name format 'test');
+select cast('hello' as bpchar format 'test');
+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);
+
+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
+
+-- 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');
+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');
+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
+create temp table tcast0 as select '1'::text as a;
+select cast(a as d1 format 'YYYY-MM-DD') from tcast0;
+
+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);
+
+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;
+
+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;
+drop domain d1, d2;
\ No newline at end of file
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/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');
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;
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 +706,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,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');
-- 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
--
2.34.1
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-06-29 18:13 Robert Haas <[email protected]>
parent: Haibo Yan <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: Robert Haas @ 2026-06-29 18:13 UTC (permalink / raw)
To: Haibo Yan <[email protected]>; +Cc: Corey Huinker <[email protected]>; jian he <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; pgsql-hackers
On Mon, Jun 29, 2026 at 1:20 AM Haibo Yan <[email protected]> wrote:
> 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.
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-06-29 21:49 Zsolt Parragi <[email protected]>
parent: jian he <[email protected]>
0 siblings, 0 replies; 53+ messages in thread
From: Zsolt Parragi @ 2026-06-29 21:49 UTC (permalink / raw)
To: [email protected]
The latest patch fail make check in my testrun:
not ok 166 + collate.linux.utf8
+ if ((str == NULL || fmtisnull) && flinfo->fn_strict)
+ return (Datum) 0; /* just return null result */
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.
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-06-30 17:39 Haibo Yan <[email protected]>
parent: Robert Haas <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: Haibo Yan @ 2026-06-30 17:39 UTC (permalink / raw)
To: Robert Haas <[email protected]>; +Cc: Corey Huinker <[email protected]>; jian he <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; pgsql-hackers
On Mon, Jun 29, 2026 at 11:14 AM Robert Haas <[email protected]> wrote:
>
> On Mon, Jun 29, 2026 at 1:20 AM Haibo Yan <[email protected]> wrote:
> > 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.
>
> --
> Robert Haas
> EDB: http://www.enterprisedb.com
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
Attachments:
[application/octet-stream] v2-0004-Add-built-in-format-casts-for-CAST-FORMAT.patch (43.5K, ../../CABXr29HF+AHV0FNxQfHyN-ByW6-3+pTBe5Pxm23wRpBYOmfohA@mail.gmail.com/2-v2-0004-Add-built-in-format-casts-for-CAST-FORMAT.patch)
download | inline diff:
From 0e1e899955dbedbadc909c6ca54c9ddcfb545b73 Mon Sep 17 00:00:00 2001
From: Haibo Yan <[email protected]>
Date: Sun, 28 Jun 2026 13:28:19 -0700
Subject: [PATCH 4/4] Add built-in format casts for CAST ... FORMAT
Add built-in format cast functions and pg_format_cast bootstrap rows for the
initial datetime/string formatted casts:
text/varchar/bpchar <-> date
text/varchar/bpchar <-> timestamp
text/varchar/bpchar <-> timestamptz
The wrappers reuse PostgreSQL's existing formatting code and are reached
through pg_format_cast, not by parser or analyzer special cases. They are
STRICT, so NULL input or NULL FORMAT returns NULL.
This provides SQL-standard-style CAST ... FORMAT support for the common
datetime/string cases, while keeping PostgreSQL extensions from the
generic infrastructure: FORMAT may be any expression coercible to text,
and format cast lookup remains catalog-driven. The patch does not claim
complete SQL-standard datetime-template conformance; time, timetz,
numeric, interval, and fuller template coverage are left for future work.
Built-in format cast rows are treated as system objects and are not dumped
by pg_dump.
---
src/backend/utils/adt/formatting.c | 317 +++++++++++++++++++++
src/bin/pg_dump/pg_dump.c | 25 +-
src/include/catalog/pg_format_cast.dat | 71 +++++
src/include/catalog/pg_proc.dat | 75 +++++
src/test/regress/expected/expressions.out | 25 +-
src/test/regress/expected/format_casts.out | 279 +++++++++++++++++-
src/test/regress/sql/expressions.sql | 17 +-
src/test/regress/sql/format_casts.sql | 128 ++++++++-
8 files changed, 914 insertions(+), 23 deletions(-)
create mode 100644 src/include/catalog/pg_format_cast.dat
diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c
index d52d71b0a8c..d42dea166b6 100644
--- a/src/backend/utils/adt/formatting.c
+++ b/src/backend/utils/adt/formatting.c
@@ -4138,6 +4138,323 @@ to_date(PG_FUNCTION_ARGS)
PG_RETURN_DATEADT(result);
}
+/* ----------
+ * Built-in format casts for CAST ( <operand> AS <type> FORMAT <template> )
+ *
+ * These wrappers are registered in pg_format_cast (see pg_format_cast.dat) and
+ * reached through a CoerceViaFormatCast node. They reuse PostgreSQL's existing
+ * formatting code: to_date()/to_timestamp()/parse_datetime() for the
+ * string -> datetime direction and timestamp_to_char()/timestamptz_to_char()
+ * for the datetime -> string direction, so the accepted template tokens are
+ * those of to_char()/to_date().
+ *
+ * There is a separate wrapper per exact source/target type pair, because
+ * pg_format_cast lookup matches the exact source and target types and text,
+ * varchar and bpchar are distinct on each side. bpchar sources are
+ * right-trimmed first. The returned text varlena is a valid varchar/bpchar
+ * value; any declared length/typmod is enforced by the ordinary coercion
+ * layered above the CoerceViaFormatCast node.
+ *
+ * All of these functions are STRICT, so a NULL source value or NULL FORMAT
+ * expression yields NULL without the C code being called.
+ * ----------
+ */
+
+/*
+ * Strip blank padding from a bpchar argument, returning a plain text value.
+ * Standard-style character casts must not fail merely because a fixed-length
+ * CHAR(n) source carries trailing spaces, so bpchar sources are right-trimmed
+ * before the format template is applied.
+ *
+ * bpchar padding uses ordinary ASCII space bytes, so we trim only trailing
+ * ' ' bytes here. This is deliberately not a general Unicode whitespace trim.
+ */
+static text *
+formatcast_rtrim_blanks(text *src)
+{
+ char *data = VARDATA_ANY(src);
+ int len = VARSIZE_ANY_EXHDR(src);
+ int orig_len = len;
+
+ while (len > 0 && data[len - 1] == ' ')
+ len--;
+
+ /* Avoid a copy when there was no trailing padding to trim. */
+ if (len == orig_len)
+ return src;
+
+ return cstring_to_text_with_len(data, len);
+}
+
+/*
+ * string -> date: reuse to_date() verbatim, so behavior matches the
+ * SQL-callable to_date(text, text).
+ */
+static Datum
+formatcast_in_date(text *src, text *fmt, Oid collid)
+{
+ return DirectFunctionCall2Coll(to_date, collid,
+ PointerGetDatum(src), PointerGetDatum(fmt));
+}
+
+/*
+ * string -> timestamp without time zone. There is no SQL-callable function
+ * for this (to_timestamp() returns timestamptz), so we parse with
+ * parse_datetime() and build the result from the local datetime fields without
+ * consulting the session time zone. A date-only template is widened to
+ * timestamp; a zoned template is rejected, since the result type carries no
+ * zone and we must not silently fold a time zone through the session setting.
+ */
+static Datum
+formatcast_in_timestamp(text *src, text *fmt, Oid collid)
+{
+ Oid typid;
+ int32 typmod;
+ int tz;
+ Datum d;
+
+ d = parse_datetime(src, fmt, collid, false, &typid, &typmod, &tz, NULL);
+
+ switch (typid)
+ {
+ case TIMESTAMPOID:
+ return d;
+ case DATEOID:
+ return TimestampGetDatum(date2timestamp_safe(DatumGetDateADT(d),
+ NULL));
+ default:
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_DATETIME_FORMAT),
+ errmsg("FORMAT template for a cast to timestamp without time zone must not specify a time zone")));
+ return (Datum) 0; /* keep compiler quiet */
+ }
+}
+
+/*
+ * string -> timestamp with time zone: reuse to_timestamp() verbatim, matching
+ * the SQL-callable to_timestamp(text, text).
+ */
+static Datum
+formatcast_in_timestamptz(text *src, text *fmt, Oid collid)
+{
+ return DirectFunctionCall2Coll(to_timestamp, collid,
+ PointerGetDatum(src), PointerGetDatum(fmt));
+}
+
+/* string -> date (text and varchar sources) */
+Datum
+formatcast_str_to_date(PG_FUNCTION_ARGS)
+{
+ return formatcast_in_date(PG_GETARG_TEXT_PP(0), PG_GETARG_TEXT_PP(1),
+ PG_GET_COLLATION());
+}
+
+/* string -> date (bpchar source; trailing blanks trimmed) */
+Datum
+formatcast_bpchar_to_date(PG_FUNCTION_ARGS)
+{
+ return formatcast_in_date(formatcast_rtrim_blanks(PG_GETARG_TEXT_PP(0)),
+ PG_GETARG_TEXT_PP(1), PG_GET_COLLATION());
+}
+
+/* string -> timestamp (text and varchar sources) */
+Datum
+formatcast_str_to_timestamp(PG_FUNCTION_ARGS)
+{
+ return formatcast_in_timestamp(PG_GETARG_TEXT_PP(0), PG_GETARG_TEXT_PP(1),
+ PG_GET_COLLATION());
+}
+
+/* string -> timestamp (bpchar source; trailing blanks trimmed) */
+Datum
+formatcast_bpchar_to_timestamp(PG_FUNCTION_ARGS)
+{
+ return formatcast_in_timestamp(formatcast_rtrim_blanks(PG_GETARG_TEXT_PP(0)),
+ PG_GETARG_TEXT_PP(1), PG_GET_COLLATION());
+}
+
+/* string -> timestamptz (text and varchar sources) */
+Datum
+formatcast_str_to_timestamptz(PG_FUNCTION_ARGS)
+{
+ return formatcast_in_timestamptz(PG_GETARG_TEXT_PP(0), PG_GETARG_TEXT_PP(1),
+ PG_GET_COLLATION());
+}
+
+/* string -> timestamptz (bpchar source; trailing blanks trimmed) */
+Datum
+formatcast_bpchar_to_timestamptz(PG_FUNCTION_ARGS)
+{
+ return formatcast_in_timestamptz(formatcast_rtrim_blanks(PG_GETARG_TEXT_PP(0)),
+ PG_GETARG_TEXT_PP(1), PG_GET_COLLATION());
+}
+
+/*
+ * Call a collation-aware (datetime, text) -> text formatting function,
+ * propagating a SQL NULL result. DirectFunctionCall cannot handle a SQL NULL
+ * result, so build a local FunctionCallInfo. Pass the caller's flinfo to
+ * provide a normal fmgr context; the callees used here do not depend on
+ * function-specific flinfo state.
+ */
+static Datum
+formatcast_to_char_call(FunctionCallInfo caller_fcinfo, PGFunction fn,
+ Oid collid, Datum value, Datum fmt, bool *resultnull)
+{
+ LOCAL_FCINFO(fcinfo, 2);
+ Datum result;
+
+ InitFunctionCallInfoData(*fcinfo, caller_fcinfo->flinfo, 2, collid,
+ NULL, NULL);
+ fcinfo->args[0].value = value;
+ fcinfo->args[0].isnull = false;
+ fcinfo->args[1].value = fmt;
+ fcinfo->args[1].isnull = false;
+
+ result = (*fn) (fcinfo);
+ *resultnull = fcinfo->isnull;
+ return result;
+}
+
+/*
+ * Per-source-type helpers shared by the datetime -> string wrappers below.
+ * Each formats the source value with the given template, returning the result
+ * text (with *resultnull set on an empty template, as for to_char()).
+ */
+static Datum
+formatcast_date_out(FunctionCallInfo fcinfo, bool *resultnull)
+{
+ DateADT dateVal = PG_GETARG_DATEADT(0);
+ text *fmt = PG_GETARG_TEXT_PP(1);
+ Timestamp ts = date2timestamp_safe(dateVal, NULL);
+
+ return formatcast_to_char_call(fcinfo, timestamp_to_char, PG_GET_COLLATION(),
+ TimestampGetDatum(ts), PointerGetDatum(fmt),
+ resultnull);
+}
+
+static Datum
+formatcast_timestamp_out(FunctionCallInfo fcinfo, bool *resultnull)
+{
+ return formatcast_to_char_call(fcinfo, timestamp_to_char, PG_GET_COLLATION(),
+ PG_GETARG_DATUM(0), PG_GETARG_DATUM(1),
+ resultnull);
+}
+
+static Datum
+formatcast_timestamptz_out(FunctionCallInfo fcinfo, bool *resultnull)
+{
+ return formatcast_to_char_call(fcinfo, timestamptz_to_char, PG_GET_COLLATION(),
+ PG_GETARG_DATUM(0), PG_GETARG_DATUM(1),
+ resultnull);
+}
+
+/*
+ * Exported datetime -> string wrappers. The text, varchar and bpchar targets
+ * each get a distinct C symbol so that no two built-in functions share a
+ * prosrc with a different return type; they all delegate to the per-source
+ * helper above, so there is no duplicated logic. The returned text varlena is
+ * a valid varchar/bpchar value; any declared length/typmod is enforced by the
+ * ordinary coercion layered above the CoerceViaFormatCast node.
+ */
+Datum
+formatcast_date_to_text(PG_FUNCTION_ARGS)
+{
+ bool resultnull;
+ Datum result = formatcast_date_out(fcinfo, &resultnull);
+
+ if (resultnull)
+ PG_RETURN_NULL();
+ PG_RETURN_DATUM(result);
+}
+
+Datum
+formatcast_date_to_varchar(PG_FUNCTION_ARGS)
+{
+ bool resultnull;
+ Datum result = formatcast_date_out(fcinfo, &resultnull);
+
+ if (resultnull)
+ PG_RETURN_NULL();
+ PG_RETURN_DATUM(result);
+}
+
+Datum
+formatcast_date_to_bpchar(PG_FUNCTION_ARGS)
+{
+ bool resultnull;
+ Datum result = formatcast_date_out(fcinfo, &resultnull);
+
+ if (resultnull)
+ PG_RETURN_NULL();
+ PG_RETURN_DATUM(result);
+}
+
+Datum
+formatcast_timestamp_to_text(PG_FUNCTION_ARGS)
+{
+ bool resultnull;
+ Datum result = formatcast_timestamp_out(fcinfo, &resultnull);
+
+ if (resultnull)
+ PG_RETURN_NULL();
+ PG_RETURN_DATUM(result);
+}
+
+Datum
+formatcast_timestamp_to_varchar(PG_FUNCTION_ARGS)
+{
+ bool resultnull;
+ Datum result = formatcast_timestamp_out(fcinfo, &resultnull);
+
+ if (resultnull)
+ PG_RETURN_NULL();
+ PG_RETURN_DATUM(result);
+}
+
+Datum
+formatcast_timestamp_to_bpchar(PG_FUNCTION_ARGS)
+{
+ bool resultnull;
+ Datum result = formatcast_timestamp_out(fcinfo, &resultnull);
+
+ if (resultnull)
+ PG_RETURN_NULL();
+ PG_RETURN_DATUM(result);
+}
+
+Datum
+formatcast_timestamptz_to_text(PG_FUNCTION_ARGS)
+{
+ bool resultnull;
+ Datum result = formatcast_timestamptz_out(fcinfo, &resultnull);
+
+ if (resultnull)
+ PG_RETURN_NULL();
+ PG_RETURN_DATUM(result);
+}
+
+Datum
+formatcast_timestamptz_to_varchar(PG_FUNCTION_ARGS)
+{
+ bool resultnull;
+ Datum result = formatcast_timestamptz_out(fcinfo, &resultnull);
+
+ if (resultnull)
+ PG_RETURN_NULL();
+ PG_RETURN_DATUM(result);
+}
+
+Datum
+formatcast_timestamptz_to_bpchar(PG_FUNCTION_ARGS)
+{
+ bool resultnull;
+ Datum result = formatcast_timestamptz_out(fcinfo, &resultnull);
+
+ if (resultnull)
+ PG_RETURN_NULL();
+ PG_RETURN_DATUM(result);
+}
+
/*
* Convert the 'date_txt' input to a datetime type using argument 'fmt'
* as a format string. The collation 'collid' may be used for case-folding
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 6517a450daa..3c0387ab347 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -2188,6 +2188,29 @@ selectDumpableCast(CastInfo *cast, Archive *fout)
DUMP_COMPONENT_ALL : DUMP_COMPONENT_NONE;
}
+/*
+ * selectDumpableFormatCast: policy-setting subroutine
+ * Mark a format cast as to be dumped or not
+ *
+ * Like casts, format_casts have no namespace and no identifiable owner, so we
+ * distinguish the built-in (initdb-created) format_casts from user-defined ones
+ * by checking whether the format cast's OID is in the range reserved for initdb.
+ * Built-in format casts are part of the system catalogs and must not be dumped
+ * as CREATE FORMAT CAST commands.
+ */
+static void
+selectDumpableFormatCast(FormatCastInfo *formatcast, Archive *fout)
+{
+ if (checkExtensionMembership(&formatcast->dobj, fout))
+ return; /* extension membership overrides all else */
+
+ if (formatcast->dobj.catId.oid <= g_last_builtin_oid)
+ formatcast->dobj.dump = DUMP_COMPONENT_NONE;
+ else
+ formatcast->dobj.dump = fout->dopt->include_everything ?
+ DUMP_COMPONENT_ALL : DUMP_COMPONENT_NONE;
+}
+
/*
* selectDumpableProcLang: policy-setting subroutine
* Mark a procedural language as to be dumped or not
@@ -9390,7 +9413,7 @@ getFormatCasts(Archive *fout)
formatcastinfo[i].dobj.name = namebuf.data;
/* Decide whether we want to dump it */
- selectDumpableObject(&(formatcastinfo[i].dobj), fout);
+ selectDumpableFormatCast(&(formatcastinfo[i]), fout);
}
PQclear(res);
diff --git a/src/include/catalog/pg_format_cast.dat b/src/include/catalog/pg_format_cast.dat
new file mode 100644
index 00000000000..39d3ee523ad
--- /dev/null
+++ b/src/include/catalog/pg_format_cast.dat
@@ -0,0 +1,71 @@
+#----------------------------------------------------------------------
+#
+# pg_format_cast.dat
+# Initial contents of the pg_format_cast system catalog.
+#
+# These are the built-in format casts for CAST ( <operand> AS <type>
+# FORMAT <template> ). Each row maps an exact (source type, target type) pair
+# to a wrapper function with the signature
+# function(source_type, text) returns target_type
+# Lookup is by exact source and target type, so text, varchar and bpchar are
+# registered separately on each side. The wrapper functions live in
+# formatting.c and reuse PostgreSQL's existing formatting code.
+#
+# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/include/catalog/pg_format_cast.dat
+#
+#----------------------------------------------------------------------
+
+[
+
+# string -> date
+{ oid => '8668', fmtsource => 'text', fmttarget => 'date',
+ fmtfunc => 'pg_format_text_to_date(text,text)' },
+{ oid => '8669', fmtsource => 'varchar', fmttarget => 'date',
+ fmtfunc => 'pg_format_varchar_to_date(varchar,text)' },
+{ oid => '8670', fmtsource => 'bpchar', fmttarget => 'date',
+ fmtfunc => 'pg_format_bpchar_to_date(bpchar,text)' },
+
+# string -> timestamp without time zone
+{ oid => '8671', fmtsource => 'text', fmttarget => 'timestamp',
+ fmtfunc => 'pg_format_text_to_timestamp(text,text)' },
+{ oid => '8672', fmtsource => 'varchar', fmttarget => 'timestamp',
+ fmtfunc => 'pg_format_varchar_to_timestamp(varchar,text)' },
+{ oid => '8673', fmtsource => 'bpchar', fmttarget => 'timestamp',
+ fmtfunc => 'pg_format_bpchar_to_timestamp(bpchar,text)' },
+
+# string -> timestamp with time zone
+{ oid => '8674', fmtsource => 'text', fmttarget => 'timestamptz',
+ fmtfunc => 'pg_format_text_to_timestamptz(text,text)' },
+{ oid => '8675', fmtsource => 'varchar', fmttarget => 'timestamptz',
+ fmtfunc => 'pg_format_varchar_to_timestamptz(varchar,text)' },
+{ oid => '8676', fmtsource => 'bpchar', fmttarget => 'timestamptz',
+ fmtfunc => 'pg_format_bpchar_to_timestamptz(bpchar,text)' },
+
+# date -> string
+{ oid => '8677', fmtsource => 'date', fmttarget => 'text',
+ fmtfunc => 'pg_format_date_to_text(date,text)' },
+{ oid => '8678', fmtsource => 'date', fmttarget => 'varchar',
+ fmtfunc => 'pg_format_date_to_varchar(date,text)' },
+{ oid => '8679', fmtsource => 'date', fmttarget => 'bpchar',
+ fmtfunc => 'pg_format_date_to_bpchar(date,text)' },
+
+# timestamp without time zone -> string
+{ oid => '8680', fmtsource => 'timestamp', fmttarget => 'text',
+ fmtfunc => 'pg_format_timestamp_to_text(timestamp,text)' },
+{ oid => '8681', fmtsource => 'timestamp', fmttarget => 'varchar',
+ fmtfunc => 'pg_format_timestamp_to_varchar(timestamp,text)' },
+{ oid => '8682', fmtsource => 'timestamp', fmttarget => 'bpchar',
+ fmtfunc => 'pg_format_timestamp_to_bpchar(timestamp,text)' },
+
+# timestamp with time zone -> string
+{ oid => '8683', fmtsource => 'timestamptz', fmttarget => 'text',
+ fmtfunc => 'pg_format_timestamptz_to_text(timestamptz,text)' },
+{ oid => '8684', fmtsource => 'timestamptz', fmttarget => 'varchar',
+ fmtfunc => 'pg_format_timestamptz_to_varchar(timestamptz,text)' },
+{ oid => '8685', fmtsource => 'timestamptz', fmttarget => 'bpchar',
+ fmtfunc => 'pg_format_timestamptz_to_bpchar(timestamptz,text)' },
+
+]
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 402d869710b..4c0c7590939 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4847,6 +4847,81 @@
{ oid => '1780', descr => 'convert text to date',
proname => 'to_date', provolatile => 's', prorettype => 'date',
proargtypes => 'text text', prosrc => 'to_date' },
+
+# Built-in format cast support functions. These are registered in
+# pg_format_cast and called through CoerceViaFormatCast.
+{ oid => '8650', descr => 'formatted cast: text to date',
+ proname => 'pg_format_text_to_date', proisstrict => 't', provolatile => 's',
+ prorettype => 'date', proargtypes => 'text text',
+ prosrc => 'formatcast_str_to_date' },
+{ oid => '8651', descr => 'formatted cast: varchar to date',
+ proname => 'pg_format_varchar_to_date', proisstrict => 't', provolatile => 's',
+ prorettype => 'date', proargtypes => 'varchar text',
+ prosrc => 'formatcast_str_to_date' },
+{ oid => '8652', descr => 'formatted cast: bpchar to date',
+ proname => 'pg_format_bpchar_to_date', proisstrict => 't', provolatile => 's',
+ prorettype => 'date', proargtypes => 'bpchar text',
+ prosrc => 'formatcast_bpchar_to_date' },
+{ oid => '8653', descr => 'formatted cast: text to timestamp',
+ proname => 'pg_format_text_to_timestamp', proisstrict => 't', provolatile => 's',
+ prorettype => 'timestamp', proargtypes => 'text text',
+ prosrc => 'formatcast_str_to_timestamp' },
+{ oid => '8654', descr => 'formatted cast: varchar to timestamp',
+ proname => 'pg_format_varchar_to_timestamp', proisstrict => 't', provolatile => 's',
+ prorettype => 'timestamp', proargtypes => 'varchar text',
+ prosrc => 'formatcast_str_to_timestamp' },
+{ oid => '8655', descr => 'formatted cast: bpchar to timestamp',
+ proname => 'pg_format_bpchar_to_timestamp', proisstrict => 't', provolatile => 's',
+ prorettype => 'timestamp', proargtypes => 'bpchar text',
+ prosrc => 'formatcast_bpchar_to_timestamp' },
+{ oid => '8656', descr => 'formatted cast: text to timestamptz',
+ proname => 'pg_format_text_to_timestamptz', proisstrict => 't', provolatile => 's',
+ prorettype => 'timestamptz', proargtypes => 'text text',
+ prosrc => 'formatcast_str_to_timestamptz' },
+{ oid => '8657', descr => 'formatted cast: varchar to timestamptz',
+ proname => 'pg_format_varchar_to_timestamptz', proisstrict => 't', provolatile => 's',
+ prorettype => 'timestamptz', proargtypes => 'varchar text',
+ prosrc => 'formatcast_str_to_timestamptz' },
+{ oid => '8658', descr => 'formatted cast: bpchar to timestamptz',
+ proname => 'pg_format_bpchar_to_timestamptz', proisstrict => 't', provolatile => 's',
+ prorettype => 'timestamptz', proargtypes => 'bpchar text',
+ prosrc => 'formatcast_bpchar_to_timestamptz' },
+{ oid => '8659', descr => 'formatted cast: date to text',
+ proname => 'pg_format_date_to_text', proisstrict => 't', provolatile => 's',
+ prorettype => 'text', proargtypes => 'date text',
+ prosrc => 'formatcast_date_to_text' },
+{ oid => '8660', descr => 'formatted cast: date to varchar',
+ proname => 'pg_format_date_to_varchar', proisstrict => 't', provolatile => 's',
+ prorettype => 'varchar', proargtypes => 'date text',
+ prosrc => 'formatcast_date_to_varchar' },
+{ oid => '8661', descr => 'formatted cast: date to bpchar',
+ proname => 'pg_format_date_to_bpchar', proisstrict => 't', provolatile => 's',
+ prorettype => 'bpchar', proargtypes => 'date text',
+ prosrc => 'formatcast_date_to_bpchar' },
+{ oid => '8662', descr => 'formatted cast: timestamp to text',
+ proname => 'pg_format_timestamp_to_text', proisstrict => 't', provolatile => 's',
+ prorettype => 'text', proargtypes => 'timestamp text',
+ prosrc => 'formatcast_timestamp_to_text' },
+{ oid => '8663', descr => 'formatted cast: timestamp to varchar',
+ proname => 'pg_format_timestamp_to_varchar', proisstrict => 't', provolatile => 's',
+ prorettype => 'varchar', proargtypes => 'timestamp text',
+ prosrc => 'formatcast_timestamp_to_varchar' },
+{ oid => '8664', descr => 'formatted cast: timestamp to bpchar',
+ proname => 'pg_format_timestamp_to_bpchar', proisstrict => 't', provolatile => 's',
+ prorettype => 'bpchar', proargtypes => 'timestamp text',
+ prosrc => 'formatcast_timestamp_to_bpchar' },
+{ oid => '8665', descr => 'formatted cast: timestamptz to text',
+ proname => 'pg_format_timestamptz_to_text', proisstrict => 't', provolatile => 's',
+ prorettype => 'text', proargtypes => 'timestamptz text',
+ prosrc => 'formatcast_timestamptz_to_text' },
+{ oid => '8666', descr => 'formatted cast: timestamptz to varchar',
+ proname => 'pg_format_timestamptz_to_varchar', proisstrict => 't', provolatile => 's',
+ prorettype => 'varchar', proargtypes => 'timestamptz text',
+ prosrc => 'formatcast_timestamptz_to_varchar' },
+{ oid => '8667', descr => 'formatted cast: timestamptz to bpchar',
+ proname => 'pg_format_timestamptz_to_bpchar', proisstrict => 't', provolatile => 's',
+ prorettype => 'bpchar', proargtypes => 'timestamptz text',
+ prosrc => 'formatcast_timestamptz_to_bpchar' },
{ oid => '1768', descr => 'format interval to text',
proname => 'to_char', provolatile => 's', prorettype => 'text',
proargtypes => 'interval text', prosrc => 'interval_to_char' },
diff --git a/src/test/regress/expected/expressions.out b/src/test/regress/expected/expressions.out
index 1cbd05135dd..06b1c98ac36 100644
--- a/src/test/regress/expected/expressions.out
+++ b/src/test/regress/expected/expressions.out
@@ -565,21 +565,22 @@ rollback;
-- CAST(expr AS type FORMAT format_expr)
--
-- A FORMAT clause is resolved through a registered format cast (see CREATE
--- FORMAT CAST) and never falls back to an ordinary cast. No format_casts are
--- defined in this test, so these casts fail with a missing-format cast error;
--- this confirms the FORMAT clause is neither ignored nor treated as an
--- ordinary cast. An unknown-type source literal is coerced to text first,
--- so the lookup key is (text, target).
--- basic form (looks up a format cast for (text, date))
-SELECT CAST('2026-06-24' AS date FORMAT 'YYYY-MM-DD');
-ERROR: format cast from type text to type date does not exist
-LINE 1: SELECT CAST('2026-06-24' AS date FORMAT 'YYYY-MM-DD');
+-- FORMAT CAST) and never falls back to an ordinary cast. The type pairs used
+-- here have no format cast (the built-in format casts cover datetime/string pairs,
+-- not these), so these casts fail with a missing-format cast error even though an
+-- ordinary cast would succeed; this confirms the FORMAT clause is neither
+-- ignored nor treated as an ordinary cast. An unknown-type source literal is
+-- coerced to text first, so the lookup key is (text, target).
+-- basic form (looks up a format cast for (text, integer); none exists)
+SELECT CAST('42' AS integer FORMAT 'whatever');
+ERROR: format cast from type text to type integer does not exist
+LINE 1: SELECT CAST('42' AS integer FORMAT 'whatever');
^
HINT: Use CREATE FORMAT CAST to define a format cast for this type pair.
-- the format may be a general expression, not just a string literal
-SELECT CAST('2026-06-24' AS date FORMAT 'YYYY' || '-MM-DD');
-ERROR: format cast from type text to type date does not exist
-LINE 1: SELECT CAST('2026-06-24' AS date FORMAT 'YYYY' || '-MM-DD');
+SELECT CAST('42' AS integer FORMAT 'what' || 'ever');
+ERROR: format cast from type text to type integer does not exist
+LINE 1: SELECT CAST('42' AS integer FORMAT 'what' || 'ever');
^
HINT: Use CREATE FORMAT CAST to define a format cast for this type pair.
-- a no-op-looking cast must not be relabeled away; it needs a (text, text) format cast
diff --git a/src/test/regress/expected/format_casts.out b/src/test/regress/expected/format_casts.out
index cb8684080ef..54897eef4d7 100644
--- a/src/test/regress/expected/format_casts.out
+++ b/src/test/regress/expected/format_casts.out
@@ -99,7 +99,9 @@ CREATE FUNCTION fmt_bad_set(integer, text) RETURNS SETOF text
CREATE FORMAT CAST (integer AS text)
WITH FUNCTION fmt_bad_set(integer, text); -- set-returning rejected
ERROR: format cast function must not return a set
--- No pseudo-types
+-- No pseudo-types: a format cast's source/target may not be a pseudo-type, so
+-- CREATE FORMAT CAST on a polymorphic (anyelement) source is intentionally
+-- rejected.
CREATE FUNCTION fmt_anyel(anyelement, text) RETURNS text
LANGUAGE sql IMMUTABLE AS $$ SELECT $2 $$;
CREATE FORMAT CAST (anyelement AS text)
@@ -397,3 +399,278 @@ SELECT count(*) FROM pg_class WHERE relname = 'format_cast_chain_view';
0
(1 row)
+-- ====================================================================
+-- Built-in datetime/string format casts.
+-- These rows are registered in pg_format_cast and reuse PostgreSQL's
+-- existing formatting template behavior.
+-- This initial set covers date, timestamp and timestamptz with
+-- text/varchar/bpchar.
+-- ====================================================================
+-- Pin the time zone and date style so results are stable across machines.
+SET TimeZone = 'UTC';
+SET DateStyle = 'ISO, YMD';
+-- text -> date. The unknown-type literal is coerced to text before format
+-- cast lookup.
+SELECT CAST('2026-06-28' AS date FORMAT 'YYYY-MM-DD');
+ date
+------------
+ 2026-06-28
+(1 row)
+
+-- varchar source -> date
+SELECT CAST('2026-06-28'::varchar AS date FORMAT 'YYYY-MM-DD');
+ date
+------------
+ 2026-06-28
+(1 row)
+
+-- bpchar source -> date: a CHAR(10) holds the value exactly, and a wider
+-- CHAR(20) pads with blanks that the format cast trims before parsing.
+SELECT CAST('2026-06-28'::char(10) AS date FORMAT 'YYYY-MM-DD');
+ date
+------------
+ 2026-06-28
+(1 row)
+
+SELECT CAST('2026-06-28'::char(20) AS date FORMAT 'YYYY-MM-DD');
+ date
+------------
+ 2026-06-28
+(1 row)
+
+-- The built-in format cast functions are STRICT: a NULL source or a NULL FORMAT
+-- expression yields NULL without invoking the format cast.
+SELECT CAST(NULL::text AS date FORMAT 'YYYY-MM-DD') IS NULL AS null_src;
+ null_src
+----------
+ t
+(1 row)
+
+SELECT CAST('2026-06-28' AS date FORMAT NULL::text) IS NULL AS null_fmt;
+ null_fmt
+----------
+ t
+(1 row)
+
+SELECT CAST(NULL::date AS text FORMAT 'YYYY-MM-DD') IS NULL AS null_src_out;
+ null_src_out
+--------------
+ t
+(1 row)
+
+-- date -> text / varchar
+SELECT CAST(date '2026-06-28' AS text FORMAT 'YYYY-MM-DD');
+ text
+------------
+ 2026-06-28
+(1 row)
+
+SELECT CAST(date '2026-06-28' AS varchar FORMAT 'YYYY-MM-DD');
+ varchar
+------------
+ 2026-06-28
+(1 row)
+
+-- date -> varchar with a typmod: the declared length is enforced by the
+-- ordinary coercion layered above the format cast (here it truncates to 7).
+SELECT CAST(date '2026-06-28' AS varchar(7) FORMAT 'YYYY-MM-DD');
+ varchar
+---------
+ 2026-06
+(1 row)
+
+-- date -> bpchar: AS char(12) pads to the declared length; bare AS char is
+-- char(1), so the ordinary coercion truncates to one character. The wrapper
+-- returns a text varlena that is a valid bpchar value.
+SELECT CAST(date '2026-06-28' AS char(12) FORMAT 'YYYY-MM-DD');
+ bpchar
+--------------
+ 2026-06-28
+(1 row)
+
+SELECT CAST(date '2026-06-28' AS char FORMAT 'YYYY-MM-DD');
+ bpchar
+--------
+ 2
+(1 row)
+
+-- Collation: a formatted cast is collated like the function call it executes.
+-- A collatable result type (text) must get a real result collation, not
+-- InvalidOid; "collation for" reports "default" (it would be NULL if the
+-- result collation were unset), and an explicit COLLATE on the result works.
+SELECT pg_collation_for(CAST(date '2026-06-28' AS text FORMAT 'YYYY-MM-DD'));
+ pg_collation_for
+------------------
+ "default"
+(1 row)
+
+SELECT pg_collation_for(CAST(date '2026-06-28' AS text FORMAT 'YYYY-MM-DD') COLLATE "C");
+ pg_collation_for
+------------------
+ "C"
+(1 row)
+
+-- An explicit COLLATE on the FORMAT argument flows in as the input collation.
+SELECT pg_collation_for(CAST(date '2026-06-28' AS text
+ FORMAT 'YYYY-MM-DD'::text COLLATE "C"));
+ pg_collation_for
+------------------
+ "C"
+(1 row)
+
+-- timestamp without time zone, both directions
+SELECT CAST('2026-06-28 13:45:30' AS timestamp FORMAT 'YYYY-MM-DD HH24:MI:SS');
+ timestamp
+---------------------
+ 2026-06-28 13:45:30
+(1 row)
+
+SELECT CAST(timestamp '2026-06-28 13:45:30' AS text FORMAT 'YYYY-MM-DD HH24:MI:SS');
+ text
+---------------------
+ 2026-06-28 13:45:30
+(1 row)
+
+-- a date-only template is widened to timestamp (midnight)
+SELECT CAST('2026-06-28' AS timestamp FORMAT 'YYYY-MM-DD');
+ timestamp
+---------------------
+ 2026-06-28 00:00:00
+(1 row)
+
+-- The built-ins reuse PostgreSQL's existing formatting parser: a template with
+-- no time-zone field consumes only the fields it names, so a trailing time zone
+-- in the input is simply ignored (existing to_timestamp() behavior, unchanged).
+SELECT CAST('2026-06-28 13:45:30+05' AS timestamp FORMAT 'YYYY-MM-DD HH24:MI:SS');
+ timestamp
+---------------------
+ 2026-06-28 13:45:30
+(1 row)
+
+-- But a template that explicitly contains a time-zone field is rejected for a
+-- timestamp-without-time-zone target (we must not fold a zone into a zoneless
+-- result via the session TimeZone).
+SELECT CAST('2026-06-28 13:45:30+05' AS timestamp FORMAT 'YYYY-MM-DD HH24:MI:SS TZH');
+ERROR: FORMAT template for a cast to timestamp without time zone must not specify a time zone
+-- timestamp WITHOUT time zone must not depend on the session TimeZone: the same
+-- (unzoned) input/template yields the same wall-clock value under any zone.
+SET TimeZone = 'America/Los_Angeles';
+SELECT CAST('2026-06-28 13:45:30' AS timestamp FORMAT 'YYYY-MM-DD HH24:MI:SS');
+ timestamp
+---------------------
+ 2026-06-28 13:45:30
+(1 row)
+
+SET TimeZone = 'UTC';
+SELECT CAST('2026-06-28 13:45:30' AS timestamp FORMAT 'YYYY-MM-DD HH24:MI:SS');
+ timestamp
+---------------------
+ 2026-06-28 13:45:30
+(1 row)
+
+-- timestamp with time zone, both directions (TimeZone is UTC, set above)
+SELECT CAST('2026-06-28 13:45:30 +00' AS timestamptz FORMAT 'YYYY-MM-DD HH24:MI:SS TZH');
+ timestamptz
+------------------------
+ 2026-06-28 13:45:30+00
+(1 row)
+
+SELECT CAST(timestamptz '2026-06-28 13:45:30+00' AS text FORMAT 'YYYY-MM-DD HH24:MI:SS TZH');
+ text
+-------------------------
+ 2026-06-28 13:45:30 +00
+(1 row)
+
+-- A sampling of datetime template tokens, exercised through the existing
+-- to_char/to_timestamp engine. TZM and SSSSS are supported here.
+SELECT CAST(timestamptz '2026-06-28 13:45:30+00' AS text
+ FORMAT 'YYYY-MM-DD"T"HH24:MI:SS TZH:TZM');
+ text
+----------------------------
+ 2026-06-28T13:45:30 +00:00
+(1 row)
+
+SELECT CAST(timestamp '2026-06-28 13:45:30' AS text FORMAT 'SSSSS');
+ text
+-------
+ 49530
+(1 row)
+
+-- Fractional seconds (FFn) are accepted by to_char in the output direction.
+SELECT CAST(timestamp '2026-06-28 13:45:30.123456' AS text
+ FORMAT 'HH24:MI:SS.FF6');
+ text
+-----------------
+ 13:45:30.123456
+(1 row)
+
+-- Empty FORMAT template behavior follows the underlying formatting functions.
+-- In the parse direction the underlying to_date() engine parses no fields and
+-- returns the all-defaults date; in the format direction the wrappers mirror
+-- to_char(value, ''), which is NULL.
+SELECT CAST('2026-06-28' AS date FORMAT '');
+ date
+---------------
+ 0001-01-01 BC
+(1 row)
+
+SELECT CAST(date '2026-06-28' AS text FORMAT '') IS NULL AS empty_fmt_out;
+ empty_fmt_out
+---------------
+ t
+(1 row)
+
+-- The FORMAT clause remains a PostgreSQL extension: it may be any expression
+-- coercible to text, not just a string literal.
+SELECT CAST('2026-' || '06-28' AS date FORMAT 'YYYY-' || 'MM-DD');
+ date
+------------
+ 2026-06-28
+(1 row)
+
+-- A FORMAT clause never falls back to an ordinary cast: an integer -> date
+-- pair has no format cast (and no ordinary cast), so it errors.
+SELECT CAST(5 AS date FORMAT 'YYYY');
+ERROR: format cast from type integer to type date does not exist
+LINE 1: SELECT CAST(5 AS date FORMAT 'YYYY');
+ ^
+HINT: Use CREATE FORMAT CAST to define a format cast for this type pair.
+-- A built-in format cast occupies its (source, target) pair, so a user
+-- CREATE FORMAT CAST for the same pair fails with the usual duplicate error.
+CREATE FUNCTION my_text_to_date(text, text) RETURNS date
+ LANGUAGE sql IMMUTABLE RETURN to_date($1, $2);
+CREATE FORMAT CAST (text AS date)
+ WITH FUNCTION my_text_to_date(text, text); -- fails: already exists
+ERROR: format cast from type text to type date already exists
+DROP FUNCTION my_text_to_date(text, text);
+-- Built-in format cast rows are system objects (their OIDs are in the pinned
+-- range), so they cannot be dropped.
+DROP FORMAT CAST (text AS date); -- fails: pinned system object
+ERROR: cannot drop format cast from text to date because it is required by the database system
+-- A view over a built-in formatted cast deparses back to CAST(... FORMAT ...).
+CREATE VIEW builtin_format_cast_view AS
+ SELECT CAST('2026-06-28' AS date FORMAT 'YYYY-MM-DD') AS d;
+SELECT pg_get_viewdef('builtin_format_cast_view'::regclass, true);
+ pg_get_viewdef
+--------------------------------------------------------------------------
+ SELECT CAST('2026-06-28'::text AS date FORMAT 'YYYY-MM-DD'::text) AS d;
+(1 row)
+
+SELECT * FROM builtin_format_cast_view;
+ d
+------------
+ 2026-06-28
+(1 row)
+
+DROP VIEW builtin_format_cast_view;
+-- Make sure query jumbling handles CoerceViaFormatCast.
+SET compute_query_id = on;
+SELECT CAST('2026-06-28' AS date FORMAT 'YYYY-MM-DD');
+ date
+------------
+ 2026-06-28
+(1 row)
+
+RESET compute_query_id;
+RESET TimeZone;
+RESET DateStyle;
diff --git a/src/test/regress/sql/expressions.sql b/src/test/regress/sql/expressions.sql
index df4f5b20466..60ca2d49d06 100644
--- a/src/test/regress/sql/expressions.sql
+++ b/src/test/regress/sql/expressions.sql
@@ -306,17 +306,18 @@ rollback;
-- CAST(expr AS type FORMAT format_expr)
--
-- A FORMAT clause is resolved through a registered format cast (see CREATE
--- FORMAT CAST) and never falls back to an ordinary cast. No format_casts are
--- defined in this test, so these casts fail with a missing-format cast error;
--- this confirms the FORMAT clause is neither ignored nor treated as an
--- ordinary cast. An unknown-type source literal is coerced to text first,
--- so the lookup key is (text, target).
+-- FORMAT CAST) and never falls back to an ordinary cast. The type pairs used
+-- here have no format cast (the built-in format casts cover datetime/string pairs,
+-- not these), so these casts fail with a missing-format cast error even though an
+-- ordinary cast would succeed; this confirms the FORMAT clause is neither
+-- ignored nor treated as an ordinary cast. An unknown-type source literal is
+-- coerced to text first, so the lookup key is (text, target).
--- basic form (looks up a format cast for (text, date))
-SELECT CAST('2026-06-24' AS date FORMAT 'YYYY-MM-DD');
+-- basic form (looks up a format cast for (text, integer); none exists)
+SELECT CAST('42' AS integer FORMAT 'whatever');
-- the format may be a general expression, not just a string literal
-SELECT CAST('2026-06-24' AS date FORMAT 'YYYY' || '-MM-DD');
+SELECT CAST('42' AS integer FORMAT 'what' || 'ever');
-- a no-op-looking cast must not be relabeled away; it needs a (text, text) format cast
SELECT CAST('abc'::text AS text FORMAT 'whatever');
diff --git a/src/test/regress/sql/format_casts.sql b/src/test/regress/sql/format_casts.sql
index 05d3236b5ac..a8c7314cfbb 100644
--- a/src/test/regress/sql/format_casts.sql
+++ b/src/test/regress/sql/format_casts.sql
@@ -81,7 +81,9 @@ CREATE FUNCTION fmt_bad_set(integer, text) RETURNS SETOF text
CREATE FORMAT CAST (integer AS text)
WITH FUNCTION fmt_bad_set(integer, text); -- set-returning rejected
--- No pseudo-types
+-- No pseudo-types: a format cast's source/target may not be a pseudo-type, so
+-- CREATE FORMAT CAST on a polymorphic (anyelement) source is intentionally
+-- rejected.
CREATE FUNCTION fmt_anyel(anyelement, text) RETURNS text
LANGUAGE sql IMMUTABLE AS $$ SELECT $2 $$;
CREATE FORMAT CAST (anyelement AS text)
@@ -263,3 +265,127 @@ DROP FUNCTION i2t_chain(integer, text) CASCADE; -- drops format cast and view
SELECT count(*) FROM pg_format_cast
WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype;
SELECT count(*) FROM pg_class WHERE relname = 'format_cast_chain_view';
+
+-- ====================================================================
+-- Built-in datetime/string format casts.
+-- These rows are registered in pg_format_cast and reuse PostgreSQL's
+-- existing formatting template behavior.
+-- This initial set covers date, timestamp and timestamptz with
+-- text/varchar/bpchar.
+-- ====================================================================
+-- Pin the time zone and date style so results are stable across machines.
+SET TimeZone = 'UTC';
+SET DateStyle = 'ISO, YMD';
+
+-- text -> date. The unknown-type literal is coerced to text before format
+-- cast lookup.
+SELECT CAST('2026-06-28' AS date FORMAT 'YYYY-MM-DD');
+-- varchar source -> date
+SELECT CAST('2026-06-28'::varchar AS date FORMAT 'YYYY-MM-DD');
+-- bpchar source -> date: a CHAR(10) holds the value exactly, and a wider
+-- CHAR(20) pads with blanks that the format cast trims before parsing.
+SELECT CAST('2026-06-28'::char(10) AS date FORMAT 'YYYY-MM-DD');
+SELECT CAST('2026-06-28'::char(20) AS date FORMAT 'YYYY-MM-DD');
+
+-- The built-in format cast functions are STRICT: a NULL source or a NULL FORMAT
+-- expression yields NULL without invoking the format cast.
+SELECT CAST(NULL::text AS date FORMAT 'YYYY-MM-DD') IS NULL AS null_src;
+SELECT CAST('2026-06-28' AS date FORMAT NULL::text) IS NULL AS null_fmt;
+SELECT CAST(NULL::date AS text FORMAT 'YYYY-MM-DD') IS NULL AS null_src_out;
+
+-- date -> text / varchar
+SELECT CAST(date '2026-06-28' AS text FORMAT 'YYYY-MM-DD');
+SELECT CAST(date '2026-06-28' AS varchar FORMAT 'YYYY-MM-DD');
+-- date -> varchar with a typmod: the declared length is enforced by the
+-- ordinary coercion layered above the format cast (here it truncates to 7).
+SELECT CAST(date '2026-06-28' AS varchar(7) FORMAT 'YYYY-MM-DD');
+-- date -> bpchar: AS char(12) pads to the declared length; bare AS char is
+-- char(1), so the ordinary coercion truncates to one character. The wrapper
+-- returns a text varlena that is a valid bpchar value.
+SELECT CAST(date '2026-06-28' AS char(12) FORMAT 'YYYY-MM-DD');
+SELECT CAST(date '2026-06-28' AS char FORMAT 'YYYY-MM-DD');
+
+-- Collation: a formatted cast is collated like the function call it executes.
+-- A collatable result type (text) must get a real result collation, not
+-- InvalidOid; "collation for" reports "default" (it would be NULL if the
+-- result collation were unset), and an explicit COLLATE on the result works.
+SELECT pg_collation_for(CAST(date '2026-06-28' AS text FORMAT 'YYYY-MM-DD'));
+SELECT pg_collation_for(CAST(date '2026-06-28' AS text FORMAT 'YYYY-MM-DD') COLLATE "C");
+-- An explicit COLLATE on the FORMAT argument flows in as the input collation.
+SELECT pg_collation_for(CAST(date '2026-06-28' AS text
+ FORMAT 'YYYY-MM-DD'::text COLLATE "C"));
+
+-- timestamp without time zone, both directions
+SELECT CAST('2026-06-28 13:45:30' AS timestamp FORMAT 'YYYY-MM-DD HH24:MI:SS');
+SELECT CAST(timestamp '2026-06-28 13:45:30' AS text FORMAT 'YYYY-MM-DD HH24:MI:SS');
+-- a date-only template is widened to timestamp (midnight)
+SELECT CAST('2026-06-28' AS timestamp FORMAT 'YYYY-MM-DD');
+-- The built-ins reuse PostgreSQL's existing formatting parser: a template with
+-- no time-zone field consumes only the fields it names, so a trailing time zone
+-- in the input is simply ignored (existing to_timestamp() behavior, unchanged).
+SELECT CAST('2026-06-28 13:45:30+05' AS timestamp FORMAT 'YYYY-MM-DD HH24:MI:SS');
+-- But a template that explicitly contains a time-zone field is rejected for a
+-- timestamp-without-time-zone target (we must not fold a zone into a zoneless
+-- result via the session TimeZone).
+SELECT CAST('2026-06-28 13:45:30+05' AS timestamp FORMAT 'YYYY-MM-DD HH24:MI:SS TZH');
+-- timestamp WITHOUT time zone must not depend on the session TimeZone: the same
+-- (unzoned) input/template yields the same wall-clock value under any zone.
+SET TimeZone = 'America/Los_Angeles';
+SELECT CAST('2026-06-28 13:45:30' AS timestamp FORMAT 'YYYY-MM-DD HH24:MI:SS');
+SET TimeZone = 'UTC';
+SELECT CAST('2026-06-28 13:45:30' AS timestamp FORMAT 'YYYY-MM-DD HH24:MI:SS');
+
+-- timestamp with time zone, both directions (TimeZone is UTC, set above)
+SELECT CAST('2026-06-28 13:45:30 +00' AS timestamptz FORMAT 'YYYY-MM-DD HH24:MI:SS TZH');
+SELECT CAST(timestamptz '2026-06-28 13:45:30+00' AS text FORMAT 'YYYY-MM-DD HH24:MI:SS TZH');
+
+-- A sampling of datetime template tokens, exercised through the existing
+-- to_char/to_timestamp engine. TZM and SSSSS are supported here.
+SELECT CAST(timestamptz '2026-06-28 13:45:30+00' AS text
+ FORMAT 'YYYY-MM-DD"T"HH24:MI:SS TZH:TZM');
+SELECT CAST(timestamp '2026-06-28 13:45:30' AS text FORMAT 'SSSSS');
+-- Fractional seconds (FFn) are accepted by to_char in the output direction.
+SELECT CAST(timestamp '2026-06-28 13:45:30.123456' AS text
+ FORMAT 'HH24:MI:SS.FF6');
+
+-- Empty FORMAT template behavior follows the underlying formatting functions.
+-- In the parse direction the underlying to_date() engine parses no fields and
+-- returns the all-defaults date; in the format direction the wrappers mirror
+-- to_char(value, ''), which is NULL.
+SELECT CAST('2026-06-28' AS date FORMAT '');
+SELECT CAST(date '2026-06-28' AS text FORMAT '') IS NULL AS empty_fmt_out;
+
+-- The FORMAT clause remains a PostgreSQL extension: it may be any expression
+-- coercible to text, not just a string literal.
+SELECT CAST('2026-' || '06-28' AS date FORMAT 'YYYY-' || 'MM-DD');
+
+-- A FORMAT clause never falls back to an ordinary cast: an integer -> date
+-- pair has no format cast (and no ordinary cast), so it errors.
+SELECT CAST(5 AS date FORMAT 'YYYY');
+
+-- A built-in format cast occupies its (source, target) pair, so a user
+-- CREATE FORMAT CAST for the same pair fails with the usual duplicate error.
+CREATE FUNCTION my_text_to_date(text, text) RETURNS date
+ LANGUAGE sql IMMUTABLE RETURN to_date($1, $2);
+CREATE FORMAT CAST (text AS date)
+ WITH FUNCTION my_text_to_date(text, text); -- fails: already exists
+DROP FUNCTION my_text_to_date(text, text);
+
+-- Built-in format cast rows are system objects (their OIDs are in the pinned
+-- range), so they cannot be dropped.
+DROP FORMAT CAST (text AS date); -- fails: pinned system object
+
+-- A view over a built-in formatted cast deparses back to CAST(... FORMAT ...).
+CREATE VIEW builtin_format_cast_view AS
+ SELECT CAST('2026-06-28' AS date FORMAT 'YYYY-MM-DD') AS d;
+SELECT pg_get_viewdef('builtin_format_cast_view'::regclass, true);
+SELECT * FROM builtin_format_cast_view;
+DROP VIEW builtin_format_cast_view;
+
+-- Make sure query jumbling handles CoerceViaFormatCast.
+SET compute_query_id = on;
+SELECT CAST('2026-06-28' AS date FORMAT 'YYYY-MM-DD');
+RESET compute_query_id;
+
+RESET TimeZone;
+RESET DateStyle;
--
2.54.0
[application/octet-stream] v2-0002-Add-pg_format_cast-and-CREATE-DROP-FORMAT-CAST.patch (74.9K, ../../CABXr29HF+AHV0FNxQfHyN-ByW6-3+pTBe5Pxm23wRpBYOmfohA@mail.gmail.com/3-v2-0002-Add-pg_format_cast-and-CREATE-DROP-FORMAT-CAST.patch)
download | inline diff:
From 3da5100614868343b6e6fd2ff395aa3021ba86c4 Mon Sep 17 00:00:00 2001
From: Haibo Yan <[email protected]>
Date: Wed, 24 Jun 2026 14:52:46 -0700
Subject: [PATCH 2/4] Add pg_format_cast and CREATE/DROP FORMAT CAST
Add pg_format_cast, a catalog for registering formatted conversions by
source and target type. A format cast function has the signature
function(source_type, text) returns target_type
where the second argument receives the FORMAT expression coerced to text.
Add CREATE FORMAT CAST and DROP FORMAT CAST, syscache support, object-address
and dependency handling, and pg_dump support. Format cast objects are
separate from pg_cast because formatted casts are always explicit,
function-backed, and keyed by a source/target type pair rather than by
ordinary cast semantics such as implicit, assignment, binary, or in/out
casts.
This patch only adds catalog and DDL infrastructure. CAST ... FORMAT
execution is added separately.
---
doc/src/sgml/catalogs.sgml | 83 +++++++
doc/src/sgml/ref/allfiles.sgml | 2 +
doc/src/sgml/ref/alter_extension.sgml | 1 +
doc/src/sgml/ref/comment.sgml | 2 +
doc/src/sgml/ref/create_format_cast.sgml | 131 +++++++++++
doc/src/sgml/ref/drop_format_cast.sgml | 126 +++++++++++
doc/src/sgml/reference.sgml | 2 +
src/backend/catalog/aclchk.c | 2 +
src/backend/catalog/dependency.c | 2 +
src/backend/catalog/objectaddress.c | 105 +++++++++
src/backend/commands/Makefile | 1 +
src/backend/commands/dropcmds.c | 22 ++
src/backend/commands/event_trigger.c | 2 +
src/backend/commands/formatcastcmds.c | 245 +++++++++++++++++++++
src/backend/commands/meson.build | 1 +
src/backend/commands/seclabel.c | 1 +
src/backend/parser/gram.y | 56 ++++-
src/backend/tcop/utility.c | 17 ++
src/bin/pg_dump/common.c | 3 +
src/bin/pg_dump/pg_dump.c | 166 ++++++++++++++
src/bin/pg_dump/pg_dump.h | 10 +
src/bin/pg_dump/pg_dump_sort.c | 9 +
src/include/catalog/Makefile | 1 +
src/include/catalog/catversion.h | 2 +-
src/include/catalog/meson.build | 1 +
src/include/catalog/pg_format_cast.h | 62 ++++++
src/include/commands/formatcast.h | 24 ++
src/include/nodes/parsenodes.h | 17 ++
src/include/tcop/cmdtaglist.h | 2 +
src/test/regress/expected/format_casts.out | 156 +++++++++++++
src/test/regress/expected/oidjoins.out | 3 +
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/format_casts.sql | 126 +++++++++++
src/tools/pgindent/typedefs.list | 4 +
34 files changed, 1386 insertions(+), 3 deletions(-)
create mode 100644 doc/src/sgml/ref/create_format_cast.sgml
create mode 100644 doc/src/sgml/ref/drop_format_cast.sgml
create mode 100644 src/backend/commands/formatcastcmds.c
create mode 100644 src/include/catalog/pg_format_cast.h
create mode 100644 src/include/commands/formatcast.h
create mode 100644 src/test/regress/expected/format_casts.out
create mode 100644 src/test/regress/sql/format_casts.sql
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 4b474c13917..de1a908221a 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -170,6 +170,11 @@
<entry>additional foreign table information</entry>
</row>
+ <row>
+ <entry><link linkend="catalog-pg-format-cast"><structname>pg_format_cast</structname></link></entry>
+ <entry>format cast functions for formatted casts</entry>
+ </row>
+
<row>
<entry><link linkend="catalog-pg-index"><structname>pg_index</structname></link></entry>
<entry>additional index information</entry>
@@ -4391,6 +4396,84 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</sect1>
+ <sect1 id="catalog-pg-format-cast">
+ <title><structname>pg_format_cast</structname></title>
+
+ <indexterm zone="catalog-pg-format-cast">
+ <primary>pg_format_cast</primary>
+ </indexterm>
+
+ <para>
+ The catalog <structname>pg_format_cast</structname> stores formatting
+ conversion functions for formatted casts. A format cast is identified by a
+ source type and a target type. The associated function is called with the
+ source value and the <literal>FORMAT</literal> expression coerced to
+ <type>text</type>, and returns the target type. At most one format cast
+ exists for any given pair of source and target types. See <xref
+ linkend="sql-createformatcast"/> for more information.
+ </para>
+
+ <table>
+ <title><structname>pg_format_cast</structname> Columns</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>oid</structfield> <type>oid</type>
+ </para>
+ <para>
+ Row identifier
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>fmtsource</structfield> <type>oid</type>
+ (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>)
+ </para>
+ <para>
+ OID of the source data type of the formatted cast
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>fmttarget</structfield> <type>oid</type>
+ (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>)
+ </para>
+ <para>
+ OID of the target data type of the formatted cast
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>fmtfunc</structfield> <type>oid</type>
+ (references <link linkend="catalog-pg-proc"><structname>pg_proc</structname></link>.<structfield>oid</structfield>)
+ </para>
+ <para>
+ OID of the format cast function, which has the signature
+ <replaceable>function</replaceable>(<replaceable>source_type</replaceable>, <type>text</type>)
+ returning <replaceable>target_type</replaceable>
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect1>
+
+
<sect1 id="catalog-pg-index">
<title><structname>pg_index</structname></title>
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index e1a56c36221..07f6aff37fc 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -70,6 +70,7 @@ Complete list of usable sgml source files in this directory.
<!ENTITY createExtension SYSTEM "create_extension.sgml">
<!ENTITY createForeignDataWrapper SYSTEM "create_foreign_data_wrapper.sgml">
<!ENTITY createForeignTable SYSTEM "create_foreign_table.sgml">
+<!ENTITY createFormatCast SYSTEM "create_format_cast.sgml">
<!ENTITY createFunction SYSTEM "create_function.sgml">
<!ENTITY createGroup SYSTEM "create_group.sgml">
<!ENTITY createIndex SYSTEM "create_index.sgml">
@@ -118,6 +119,7 @@ Complete list of usable sgml source files in this directory.
<!ENTITY dropExtension SYSTEM "drop_extension.sgml">
<!ENTITY dropForeignDataWrapper SYSTEM "drop_foreign_data_wrapper.sgml">
<!ENTITY dropForeignTable SYSTEM "drop_foreign_table.sgml">
+<!ENTITY dropFormatCast SYSTEM "drop_format_cast.sgml">
<!ENTITY dropFunction SYSTEM "drop_function.sgml">
<!ENTITY dropGroup SYSTEM "drop_group.sgml">
<!ENTITY dropIndex SYSTEM "drop_index.sgml">
diff --git a/doc/src/sgml/ref/alter_extension.sgml b/doc/src/sgml/ref/alter_extension.sgml
index 60218fcd01c..afc8eba5021 100644
--- a/doc/src/sgml/ref/alter_extension.sgml
+++ b/doc/src/sgml/ref/alter_extension.sgml
@@ -39,6 +39,7 @@ ALTER EXTENSION <replaceable class="parameter">name</replaceable> DROP <replacea
EVENT TRIGGER <replaceable class="parameter">object_name</replaceable> |
FOREIGN DATA WRAPPER <replaceable class="parameter">object_name</replaceable> |
FOREIGN TABLE <replaceable class="parameter">object_name</replaceable> |
+ FORMAT CAST (<replaceable>source_type</replaceable> AS <replaceable>target_type</replaceable>) |
FUNCTION <replaceable class="parameter">function_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
MATERIALIZED VIEW <replaceable class="parameter">object_name</replaceable> |
OPERATOR <replaceable class="parameter">operator_name</replaceable> (<replaceable class="parameter">left_type</replaceable>, <replaceable class="parameter">right_type</replaceable>) |
diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 6d8479d6829..a99d0887576 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -37,6 +37,7 @@ COMMENT ON
EVENT TRIGGER <replaceable class="parameter">object_name</replaceable> |
FOREIGN DATA WRAPPER <replaceable class="parameter">object_name</replaceable> |
FOREIGN TABLE <replaceable class="parameter">object_name</replaceable> |
+ FORMAT CAST (<replaceable>source_type</replaceable> AS <replaceable>target_type</replaceable>) |
FUNCTION <replaceable class="parameter">function_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
INDEX <replaceable class="parameter">object_name</replaceable> |
LARGE OBJECT <replaceable class="parameter">large_object_oid</replaceable> |
@@ -336,6 +337,7 @@ COMMENT ON EVENT TRIGGER abort_ddl IS 'Aborts all DDL commands';
COMMENT ON EXTENSION hstore IS 'implements the hstore data type';
COMMENT ON FOREIGN DATA WRAPPER mywrapper IS 'my foreign data wrapper';
COMMENT ON FOREIGN TABLE my_foreign_table IS 'Employee Information in other database';
+COMMENT ON FORMAT CAST (integer AS text) IS 'Allow formatted casts from integer to text';
COMMENT ON FUNCTION my_function (timestamp) IS 'Returns Roman Numeral';
COMMENT ON INDEX my_index IS 'Enforces uniqueness on employee ID';
COMMENT ON LANGUAGE plpython IS 'Python support for stored procedures';
diff --git a/doc/src/sgml/ref/create_format_cast.sgml b/doc/src/sgml/ref/create_format_cast.sgml
new file mode 100644
index 00000000000..0e0cf0f7b1b
--- /dev/null
+++ b/doc/src/sgml/ref/create_format_cast.sgml
@@ -0,0 +1,131 @@
+<!--
+doc/src/sgml/ref/create_format_cast.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-createformatcast">
+ <indexterm zone="sql-createformatcast">
+ <primary>CREATE FORMAT CAST</primary>
+ </indexterm>
+
+ <refmeta>
+ <refentrytitle>CREATE FORMAT CAST</refentrytitle>
+ <manvolnum>7</manvolnum>
+ <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+ <refname>CREATE FORMAT CAST</refname>
+ <refpurpose>define a new format cast for a formatted cast</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE FORMAT CAST (<replaceable>source_type</replaceable> AS <replaceable>target_type</replaceable>)
+ WITH FUNCTION <replaceable>function_name</replaceable> [ (<replaceable>argument_type</replaceable> [, ...]) ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1 id="sql-createformatcast-description">
+ <title>Description</title>
+
+ <para>
+ <command>CREATE FORMAT CAST</command> defines a formatting conversion for
+ <literal>CAST(<replaceable>expr</replaceable> AS <replaceable>target_type</replaceable> FORMAT <replaceable>format_expr</replaceable>)</literal>.
+ A format cast is selected by the source type and target type of the cast.
+ Unlike an ordinary cast (see <xref linkend="sql-createcast"/>), a formatted
+ cast is always explicit and always uses a function, and the function
+ additionally receives the <literal>FORMAT</literal> expression.
+ </para>
+
+ <para>
+ The format cast function must take exactly two arguments and return the
+ target type:
+<synopsis>
+<replaceable>function_name</replaceable>(<replaceable>source_type</replaceable>, text) RETURNS <replaceable>target_type</replaceable>
+</synopsis>
+ The first argument is the value being formatted, and the second argument
+ receives the <literal>FORMAT</literal> expression coerced to
+ <type>text</type>. Only one format cast may be registered for a given
+ <literal>(<replaceable>source_type</replaceable>,
+ <replaceable>target_type</replaceable>)</literal> pair. Neither the source
+ type nor the target type may be a pseudo-type.
+ </para>
+
+ <para>
+ To create a format cast, you must be the owner of the source type or the
+ target type, and you must have <literal>EXECUTE</literal> privilege on the
+ format cast function.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>Parameters</title>
+
+ <variablelist>
+ <varlistentry>
+ <term><replaceable>source_type</replaceable></term>
+ <listitem>
+ <para>
+ The data type of the value passed to the format cast (the source of the
+ cast).
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable>target_type</replaceable></term>
+ <listitem>
+ <para>
+ The data type returned by the format cast (the target of the cast).
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable>function_name</replaceable>[(<replaceable>argument_type</replaceable> [, ...])]</term>
+ <listitem>
+ <para>
+ The function used to perform the formatted cast. The argument list, if
+ given, must name the two argument types
+ (<replaceable>source_type</replaceable> and <type>text</type>).
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+
+ <refsect1 id="sql-createformatcast-examples">
+ <title>Examples</title>
+
+ <para>
+ Register a format cast that converts an <type>integer</type> to
+ <type>text</type> using the supplied format string:
+<programlisting>
+CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2;
+
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION int4_to_text_fmt(integer, text);
+</programlisting></para>
+ </refsect1>
+
+ <refsect1 id="sql-createformatcast-compat">
+ <title>Compatibility</title>
+
+ <para>
+ <command>CREATE FORMAT CAST</command> is a
+ <productname>PostgreSQL</productname> extension.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>See Also</title>
+
+ <simplelist type="inline">
+ <member><xref linkend="sql-dropformatcast"/></member>
+ <member><xref linkend="sql-createcast"/></member>
+ </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/drop_format_cast.sgml b/doc/src/sgml/ref/drop_format_cast.sgml
new file mode 100644
index 00000000000..8dce02fa882
--- /dev/null
+++ b/doc/src/sgml/ref/drop_format_cast.sgml
@@ -0,0 +1,126 @@
+<!--
+doc/src/sgml/ref/drop_format_cast.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-dropformatcast">
+ <indexterm zone="sql-dropformatcast">
+ <primary>DROP FORMAT CAST</primary>
+ </indexterm>
+
+ <refmeta>
+ <refentrytitle>DROP FORMAT CAST</refentrytitle>
+ <manvolnum>7</manvolnum>
+ <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+ <refname>DROP FORMAT CAST</refname>
+ <refpurpose>remove a format cast</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP FORMAT CAST [ IF EXISTS ] (<replaceable>source_type</replaceable> AS <replaceable>target_type</replaceable>) [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1 id="sql-dropformatcast-description">
+ <title>Description</title>
+
+ <para>
+ <command>DROP FORMAT CAST</command> removes a previously defined format cast for
+ the given pair of source and target types.
+ </para>
+
+ <para>
+ Dropping a format cast requires ownership of either the source type or the
+ target type.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>Parameters</title>
+
+ <variablelist>
+ <varlistentry>
+ <term><literal>IF EXISTS</literal></term>
+ <listitem>
+ <para>
+ Do not throw an error if the format cast does not exist. A notice is
+ issued in this case.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable>source_type</replaceable></term>
+ <listitem>
+ <para>
+ The source data type of the format cast.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable>target_type</replaceable></term>
+ <listitem>
+ <para>
+ The target data type of the format cast.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>CASCADE</literal></term>
+ <listitem>
+ <para>
+ Automatically drop objects that depend on the format cast,
+ and in turn all objects that depend on those objects
+ (see <xref linkend="ddl-depend"/>).
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>RESTRICT</literal></term>
+ <listitem>
+ <para>
+ Refuse to drop the format cast if any objects depend on it. This is the
+ default.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+
+ <refsect1 id="sql-dropformatcast-examples">
+ <title>Examples</title>
+
+ <para>
+ To drop the format cast for the cast from <type>integer</type> to
+ <type>text</type>:
+<programlisting>
+DROP FORMAT CAST (integer AS text);
+</programlisting></para>
+ </refsect1>
+
+ <refsect1 id="sql-dropformatcast-compat">
+ <title>Compatibility</title>
+
+ <para>
+ <command>DROP FORMAT CAST</command> is a
+ <productname>PostgreSQL</productname> extension. See <xref
+ linkend="sql-createformatcast"/> for details.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>See Also</title>
+
+ <simplelist type="inline">
+ <member><xref linkend="sql-createformatcast"/></member>
+ </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index 674ac17e82c..ffe012ce0ed 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -98,6 +98,7 @@
&createExtension;
&createForeignDataWrapper;
&createForeignTable;
+ &createFormatCast;
&createFunction;
&createGroup;
&createIndex;
@@ -146,6 +147,7 @@
&dropExtension;
&dropForeignDataWrapper;
&dropForeignTable;
+ &dropFormatCast;
&dropFunction;
&dropGroup;
&dropIndex;
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..267cbaef9a4 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -2794,6 +2794,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_AMPROC:
case OBJECT_ATTRIBUTE:
case OBJECT_CAST:
+ case OBJECT_FORMAT_CAST:
case OBJECT_DEFAULT:
case OBJECT_DEFACL:
case OBJECT_DOMCONSTRAINT:
@@ -2937,6 +2938,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_AMPROC:
case OBJECT_ATTRIBUTE:
case OBJECT_CAST:
+ case OBJECT_FORMAT_CAST:
case OBJECT_DEFAULT:
case OBJECT_DEFACL:
case OBJECT_DOMCONSTRAINT:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index c54774b3275..ff7ab606bcc 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -41,6 +41,7 @@
#include "catalog/pg_extension.h"
#include "catalog/pg_foreign_data_wrapper.h"
#include "catalog/pg_foreign_server.h"
+#include "catalog/pg_format_cast.h"
#include "catalog/pg_init_privs.h"
#include "catalog/pg_language.h"
#include "catalog/pg_largeobject.h"
@@ -1534,6 +1535,7 @@ doDeletion(const ObjectAddress *object, int flags)
case DefaultAclRelationId:
case EventTriggerRelationId:
case TransformRelationId:
+ case FormatCastRelationId:
case AuthMemRelationId:
DropObjectById(object);
break;
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index af0e4703616..1d84be82e0d 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -37,6 +37,7 @@
#include "catalog/pg_extension.h"
#include "catalog/pg_foreign_data_wrapper.h"
#include "catalog/pg_foreign_server.h"
+#include "catalog/pg_format_cast.h"
#include "catalog/pg_language.h"
#include "catalog/pg_largeobject.h"
#include "catalog/pg_largeobject_metadata.h"
@@ -70,6 +71,7 @@
#include "commands/defrem.h"
#include "commands/event_trigger.h"
#include "commands/extension.h"
+#include "commands/formatcast.h"
#include "commands/policy.h"
#include "commands/proclang.h"
#include "commands/tablespace.h"
@@ -179,6 +181,20 @@ static const ObjectPropertyType ObjectProperty[] =
OBJECT_CAST,
false
},
+ {
+ "format cast",
+ FormatCastRelationId,
+ FormatCastOidIndexId,
+ FORMATCASTOID,
+ SYSCACHEID_INVALID,
+ Anum_pg_format_cast_oid,
+ InvalidAttrNumber,
+ InvalidAttrNumber,
+ InvalidAttrNumber,
+ InvalidAttrNumber,
+ OBJECT_FORMAT_CAST,
+ false
+ },
{
"collation",
CollationRelationId,
@@ -796,6 +812,9 @@ static const struct object_type_map
{
"cast", OBJECT_CAST
},
+ {
+ "format cast", OBJECT_FORMAT_CAST
+ },
{
"collation", OBJECT_COLLATION
},
@@ -1165,6 +1184,21 @@ get_object_address(ObjectType objtype, Node *object,
address.objectSubId = 0;
}
break;
+ case OBJECT_FORMAT_CAST:
+ {
+ TypeName *sourcetype = linitial_node(TypeName, castNode(List, object));
+ TypeName *targettype = lsecond_node(TypeName, castNode(List, object));
+ Oid sourcetypeid;
+ Oid targettypeid;
+
+ sourcetypeid = LookupTypeNameOid(NULL, sourcetype, missing_ok);
+ targettypeid = LookupTypeNameOid(NULL, targettype, missing_ok);
+ address.classId = FormatCastRelationId;
+ address.objectId =
+ get_format_cast_oid(sourcetypeid, targettypeid, missing_ok);
+ address.objectSubId = 0;
+ }
+ break;
case OBJECT_TRANSFORM:
{
TypeName *typename = linitial_node(TypeName, castNode(List, object));
@@ -2239,6 +2273,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
* exceptions.
*/
if (type == OBJECT_TYPE || type == OBJECT_DOMAIN || type == OBJECT_CAST ||
+ type == OBJECT_FORMAT_CAST ||
type == OBJECT_TRANSFORM || type == OBJECT_DOMCONSTRAINT)
{
Datum *elems;
@@ -2291,6 +2326,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
type == OBJECT_ROUTINE ||
type == OBJECT_OPERATOR ||
type == OBJECT_CAST ||
+ type == OBJECT_FORMAT_CAST ||
type == OBJECT_AMOP ||
type == OBJECT_AMPROC)
{
@@ -2336,6 +2372,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
pg_fallthrough;
case OBJECT_DOMCONSTRAINT:
case OBJECT_CAST:
+ case OBJECT_FORMAT_CAST:
case OBJECT_PUBLICATION_REL:
case OBJECT_DEFACL:
case OBJECT_TRANSFORM:
@@ -2424,6 +2461,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
objnode = (Node *) typename;
break;
case OBJECT_CAST:
+ case OBJECT_FORMAT_CAST:
case OBJECT_DOMCONSTRAINT:
case OBJECT_TRANSFORM:
objnode = (Node *) list_make2(typename, linitial(args));
@@ -2583,6 +2621,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
address.objectId)));
break;
case OBJECT_CAST:
+ case OBJECT_FORMAT_CAST:
{
/* We can only check permissions on the source/target types */
TypeName *sourcetype = linitial_node(TypeName, castNode(List, object));
@@ -3111,6 +3150,31 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
break;
}
+ case FormatCastRelationId:
+ {
+ HeapTuple fmtTup;
+ Form_pg_format_cast fmtForm;
+
+ fmtTup = SearchSysCache1(FORMATCASTOID,
+ ObjectIdGetDatum(object->objectId));
+ if (!HeapTupleIsValid(fmtTup))
+ {
+ if (!missing_ok)
+ elog(ERROR, "could not find tuple for format cast %u",
+ object->objectId);
+ break;
+ }
+
+ fmtForm = (Form_pg_format_cast) GETSTRUCT(fmtTup);
+
+ appendStringInfo(&buffer, _("format cast from %s to %s"),
+ format_type_be(fmtForm->fmtsource),
+ format_type_be(fmtForm->fmttarget));
+
+ ReleaseSysCache(fmtTup);
+ break;
+ }
+
case CollationRelationId:
{
HeapTuple collTup;
@@ -4762,6 +4826,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
appendStringInfoString(&buffer, "cast");
break;
+ case FormatCastRelationId:
+ appendStringInfoString(&buffer, "format cast");
+ break;
+
case CollationRelationId:
appendStringInfoString(&buffer, "collation");
break;
@@ -5225,6 +5293,43 @@ getObjectIdentityParts(const ObjectAddress *object,
break;
}
+ case FormatCastRelationId:
+ {
+ Relation fmtRel;
+ HeapTuple tup;
+ Form_pg_format_cast fmtForm;
+
+ fmtRel = table_open(FormatCastRelationId, AccessShareLock);
+
+ tup = get_catalog_object_by_oid(fmtRel, Anum_pg_format_cast_oid,
+ object->objectId);
+
+ if (!HeapTupleIsValid(tup))
+ {
+ if (!missing_ok)
+ elog(ERROR, "could not find tuple for format cast %u",
+ object->objectId);
+
+ table_close(fmtRel, AccessShareLock);
+ break;
+ }
+
+ fmtForm = (Form_pg_format_cast) GETSTRUCT(tup);
+
+ appendStringInfo(&buffer, "(%s AS %s)",
+ format_type_be_qualified(fmtForm->fmtsource),
+ format_type_be_qualified(fmtForm->fmttarget));
+
+ if (objname)
+ {
+ *objname = list_make1(format_type_be_qualified(fmtForm->fmtsource));
+ *objargs = list_make1(format_type_be_qualified(fmtForm->fmttarget));
+ }
+
+ table_close(fmtRel, AccessShareLock);
+ break;
+ }
+
case CollationRelationId:
{
HeapTuple collTup;
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 5b9d084977e..b6928dd6014 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -38,6 +38,7 @@ OBJS = \
explain_state.o \
extension.o \
foreigncmds.o \
+ formatcastcmds.o \
functioncmds.o \
indexcmds.o \
lockcmds.o \
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 88a2df65c69..776359dfdbf 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -25,6 +25,7 @@
#include "miscadmin.h"
#include "parser/parse_type.h"
#include "utils/acl.h"
+#include "utils/builtins.h"
#include "utils/lsyscache.h"
@@ -401,6 +402,27 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
}
}
break;
+ case OBJECT_FORMAT_CAST:
+ {
+ if (!type_in_list_does_not_exist_skipping(list_make1(linitial(castNode(List, object))), &msg, &name) &&
+ !type_in_list_does_not_exist_skipping(list_make1(lsecond(castNode(List, object))), &msg, &name))
+ {
+ /*
+ * Both types exist (else the checks above would have
+ * produced a message), so resolve them to OIDs and report
+ * them with format_type_be(). This keeps the wording
+ * consistent with the duplicate-object and undefined-object
+ * errors, which also use format_type_be().
+ */
+ Oid sourcetypeid = typenameTypeId(NULL, linitial_node(TypeName, castNode(List, object)));
+ Oid targettypeid = typenameTypeId(NULL, lsecond_node(TypeName, castNode(List, object)));
+
+ msg = gettext_noop("format cast from type %s to type %s does not exist, skipping");
+ name = format_type_be(sourcetypeid);
+ args = format_type_be(targettypeid);
+ }
+ }
+ break;
case OBJECT_TRANSFORM:
if (!type_in_list_does_not_exist_skipping(list_make1(linitial(castNode(List, object))), &msg, &name))
{
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index adc6eabc0f4..47a05a77122 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -2301,6 +2301,7 @@ stringify_grant_objtype(ObjectType objtype)
case OBJECT_AMPROC:
case OBJECT_ATTRIBUTE:
case OBJECT_CAST:
+ case OBJECT_FORMAT_CAST:
case OBJECT_COLLATION:
case OBJECT_CONVERSION:
case OBJECT_DEFAULT:
@@ -2385,6 +2386,7 @@ stringify_adefprivs_objtype(ObjectType objtype)
case OBJECT_AMPROC:
case OBJECT_ATTRIBUTE:
case OBJECT_CAST:
+ case OBJECT_FORMAT_CAST:
case OBJECT_COLLATION:
case OBJECT_CONVERSION:
case OBJECT_DEFAULT:
diff --git a/src/backend/commands/formatcastcmds.c b/src/backend/commands/formatcastcmds.c
new file mode 100644
index 00000000000..bbee0e74492
--- /dev/null
+++ b/src/backend/commands/formatcastcmds.c
@@ -0,0 +1,245 @@
+/*-------------------------------------------------------------------------
+ *
+ * formatcastcmds.c
+ * Routines for SQL commands that manipulate format casts.
+ *
+ * A format cast associates a function with a (source type, target type) pair.
+ * The function implements CAST(expr AS target FORMAT format_expr): it
+ * receives the source value and the FORMAT expression (passed as text) and
+ * returns the target type. Format casts are registered in the pg_format_cast
+ * catalog and are intentionally separate from pg_cast (see pg_format_cast.h).
+ *
+ * This file provides the catalog registration machinery (CREATE/DROP
+ * FORMAT CAST). It does not perform expression transformation or execution of
+ * formatted casts.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/commands/formatcastcmds.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "catalog/catalog.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/objectaccess.h"
+#include "catalog/objectaddress.h"
+#include "catalog/pg_format_cast.h"
+#include "catalog/pg_proc.h"
+#include "catalog/pg_type.h"
+#include "commands/formatcast.h"
+#include "miscadmin.h"
+#include "parser/parse_func.h"
+#include "parser/parse_type.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+/*
+ * Validate the signature of a format cast function: it must be a normal
+ * function (not a procedure, aggregate or window function), must not return
+ * a set, and must have the signature
+ *
+ * function(source_type, text) returns target_type
+ *
+ * NB: the format cast signature is fixed at two arguments and does not include
+ * the target type modifier; typmod-aware format cast signatures are not
+ * supported.
+ */
+static void
+check_format_cast_function(Form_pg_proc procstruct,
+ Oid sourcetypeid, Oid targettypeid)
+{
+ if (procstruct->prokind != PROKIND_FUNCTION)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("format cast function must be a normal function")));
+ if (procstruct->proretset)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("format cast function must not return a set")));
+ if (procstruct->pronargs != 2)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("format cast function must take exactly two arguments")));
+ if (procstruct->proargtypes.values[0] != sourcetypeid)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("first argument of format cast function must be type %s",
+ format_type_be(sourcetypeid))));
+ if (procstruct->proargtypes.values[1] != TEXTOID)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("second argument of format cast function must be type %s",
+ "text")));
+ if (procstruct->prorettype != targettypeid)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("return data type of format cast function must be type %s",
+ format_type_be(targettypeid))));
+}
+
+/*
+ * CREATE FORMAT CAST
+ */
+ObjectAddress
+CreateFormatCast(CreateFormatCastStmt *stmt)
+{
+ Oid sourcetypeid;
+ Oid targettypeid;
+ char sourcetyptype;
+ char targettyptype;
+ Oid funcid;
+ AclResult aclresult;
+ Form_pg_proc procstruct;
+ HeapTuple tuple;
+ HeapTuple newtuple;
+ Datum values[Natts_pg_format_cast];
+ bool nulls[Natts_pg_format_cast] = {0};
+ Oid formatcastid;
+ Relation relation;
+ ObjectAddress myself,
+ referenced;
+ ObjectAddresses *addrs;
+
+ sourcetypeid = typenameTypeId(NULL, stmt->sourcetype);
+ targettypeid = typenameTypeId(NULL, stmt->targettype);
+ sourcetyptype = get_typtype(sourcetypeid);
+ targettyptype = get_typtype(targettypeid);
+
+ /* No pseudo-types allowed */
+ if (sourcetyptype == TYPTYPE_PSEUDO)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("source data type %s is a pseudo-type",
+ TypeNameToString(stmt->sourcetype))));
+ if (targettyptype == TYPTYPE_PSEUDO)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("target data type %s is a pseudo-type",
+ TypeNameToString(stmt->targettype))));
+
+ /*
+ * Permission check. As for CREATE CAST, the caller must own at least one
+ * of the two types involved; owning a type is what authorizes defining
+ * conversion behavior for it. In addition, and following CREATE OPERATOR
+ * and CREATE AGGREGATE, we require EXECUTE permission on the format cast
+ * function (this will also be checked when the format cast is used, but it
+ * is a good idea to verify it up front). We intentionally do not require
+ * ownership of the function, unlike CREATE TRANSFORM, because a format cast
+ * is a data conversion rather than a procedural-language binding.
+ */
+ if (!object_ownercheck(TypeRelationId, sourcetypeid, GetUserId())
+ && !object_ownercheck(TypeRelationId, targettypeid, GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be owner of type %s or type %s",
+ format_type_be(sourcetypeid),
+ format_type_be(targettypeid))));
+
+ /*
+ * Look up and validate the format cast function.
+ */
+ funcid = LookupFuncWithArgs(OBJECT_FUNCTION, stmt->func, false);
+
+ aclresult = object_aclcheck(ProcedureRelationId, funcid, GetUserId(), ACL_EXECUTE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_FUNCTION,
+ NameListToString(stmt->func->objname));
+
+ tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for function %u", funcid);
+ procstruct = (Form_pg_proc) GETSTRUCT(tuple);
+ check_format_cast_function(procstruct, sourcetypeid, targettypeid);
+ ReleaseSysCache(tuple);
+
+ /*
+ * Check for a pre-existing format cast for this (source, target) pair. For
+ * this version only one format cast per pair is allowed.
+ */
+ relation = table_open(FormatCastRelationId, RowExclusiveLock);
+
+ if (SearchSysCacheExists2(FORMATCASTSOURCETARGET,
+ ObjectIdGetDatum(sourcetypeid),
+ ObjectIdGetDatum(targettypeid)))
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_OBJECT),
+ errmsg("format cast from type %s to type %s already exists",
+ format_type_be(sourcetypeid),
+ format_type_be(targettypeid))));
+
+ /*
+ * Build and insert the catalog tuple.
+ */
+ formatcastid = GetNewOidWithIndex(relation, FormatCastOidIndexId,
+ Anum_pg_format_cast_oid);
+ values[Anum_pg_format_cast_oid - 1] = ObjectIdGetDatum(formatcastid);
+ values[Anum_pg_format_cast_fmtsource - 1] = ObjectIdGetDatum(sourcetypeid);
+ values[Anum_pg_format_cast_fmttarget - 1] = ObjectIdGetDatum(targettypeid);
+ values[Anum_pg_format_cast_fmtfunc - 1] = ObjectIdGetDatum(funcid);
+
+ newtuple = heap_form_tuple(RelationGetDescr(relation), values, nulls);
+ CatalogTupleInsert(relation, newtuple);
+
+ addrs = new_object_addresses();
+
+ ObjectAddressSet(myself, FormatCastRelationId, formatcastid);
+
+ /* dependency on source type */
+ ObjectAddressSet(referenced, TypeRelationId, sourcetypeid);
+ add_exact_object_address(&referenced, addrs);
+
+ /* dependency on target type */
+ ObjectAddressSet(referenced, TypeRelationId, targettypeid);
+ add_exact_object_address(&referenced, addrs);
+
+ /* dependency on the format cast function */
+ ObjectAddressSet(referenced, ProcedureRelationId, funcid);
+ add_exact_object_address(&referenced, addrs);
+
+ record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL);
+ free_object_addresses(addrs);
+
+ /* dependency on extension */
+ recordDependencyOnCurrentExtension(&myself, false);
+
+ /* Post creation hook for new format cast */
+ InvokeObjectPostCreateHook(FormatCastRelationId, formatcastid, 0);
+
+ heap_freetuple(newtuple);
+
+ table_close(relation, RowExclusiveLock);
+
+ return myself;
+}
+
+/*
+ * get_format_cast_oid - given source and target type OIDs, look up a
+ * format cast OID
+ */
+Oid
+get_format_cast_oid(Oid sourcetypeid, Oid targettypeid, bool missing_ok)
+{
+ Oid oid;
+
+ oid = GetSysCacheOid2(FORMATCASTSOURCETARGET, Anum_pg_format_cast_oid,
+ ObjectIdGetDatum(sourcetypeid),
+ ObjectIdGetDatum(targettypeid));
+ if (!OidIsValid(oid) && !missing_ok)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("format cast from type %s to type %s does not exist",
+ format_type_be(sourcetypeid),
+ format_type_be(targettypeid))));
+ return oid;
+}
diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build
index 9f258d566eb..689ad0c16db 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -26,6 +26,7 @@ backend_sources += files(
'explain_state.c',
'extension.c',
'foreigncmds.c',
+ 'formatcastcmds.c',
'functioncmds.c',
'indexcmds.c',
'lockcmds.c',
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 77542d04200..f57a6d6acc2 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -66,6 +66,7 @@ SecLabelSupportsObjectType(ObjectType objtype)
case OBJECT_AMPROC:
case OBJECT_ATTRIBUTE:
case OBJECT_CAST:
+ case OBJECT_FORMAT_CAST:
case OBJECT_COLLATION:
case OBJECT_CONVERSION:
case OBJECT_DEFAULT:
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ef4881efc81..8ef4d22ecba 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -295,13 +295,14 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt
CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt
CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt
+ CreateFormatCastStmt
CreatePropGraphStmt AlterPropGraphStmt
CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt
CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt
DropOpClassStmt DropOpFamilyStmt DropStmt
DropCastStmt DropRoleStmt
DropdbStmt DropTableSpaceStmt
- DropTransformStmt
+ DropTransformStmt DropFormatCastStmt
DropUserMappingStmt ExplainStmt FetchStmt
GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt
ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt
@@ -1105,6 +1106,7 @@ stmt:
| CreateStatsStmt
| CreateTableSpaceStmt
| CreateTransformStmt
+ | CreateFormatCastStmt
| CreateTrigStmt
| CreateEventTrigStmt
| CreateRoleStmt
@@ -1125,6 +1127,7 @@ stmt:
| DropSubscriptionStmt
| DropTableSpaceStmt
| DropTransformStmt
+ | DropFormatCastStmt
| DropRoleStmt
| DropUserMappingStmt
| DropdbStmt
@@ -5522,6 +5525,16 @@ AlterExtensionContentsStmt:
n->object = (Node *) list_make2($7, $9);
$$ = (Node *) n;
}
+ | ALTER EXTENSION name add_drop FORMAT CAST '(' Typename AS Typename ')'
+ {
+ AlterExtensionContentsStmt *n = makeNode(AlterExtensionContentsStmt);
+
+ n->extname = $3;
+ n->action = $4;
+ n->objtype = OBJECT_FORMAT_CAST;
+ n->object = (Node *) list_make2($8, $10);
+ $$ = (Node *) n;
+ }
| ALTER EXTENSION name add_drop DOMAIN_P Typename
{
AlterExtensionContentsStmt *n = makeNode(AlterExtensionContentsStmt);
@@ -7512,6 +7525,15 @@ CommentStmt:
n->comment = $10;
$$ = (Node *) n;
}
+ | COMMENT ON FORMAT CAST '(' Typename AS Typename ')' IS comment_text
+ {
+ CommentStmt *n = makeNode(CommentStmt);
+
+ n->objtype = OBJECT_FORMAT_CAST;
+ n->object = (Node *) list_make2($6, $8);
+ n->comment = $11;
+ $$ = (Node *) n;
+ }
;
comment_text:
@@ -9893,6 +9915,38 @@ DropTransformStmt: DROP TRANSFORM opt_if_exists FOR Typename LANGUAGE name opt_d
;
+/*****************************************************************************
+ *
+ * CREATE FORMAT CAST / DROP FORMAT CAST
+ *
+ * A format cast registers the function implementing
+ * CAST(expr AS target FORMAT format_expr) for a (source, target) type pair.
+ *****************************************************************************/
+
+CreateFormatCastStmt: CREATE FORMAT CAST '(' Typename AS Typename ')' WITH FUNCTION function_with_argtypes
+ {
+ CreateFormatCastStmt *n = makeNode(CreateFormatCastStmt);
+
+ n->sourcetype = $5;
+ n->targettype = $7;
+ n->func = $11;
+ $$ = (Node *) n;
+ }
+ ;
+
+DropFormatCastStmt: DROP FORMAT CAST opt_if_exists '(' Typename AS Typename ')' opt_drop_behavior
+ {
+ DropStmt *n = makeNode(DropStmt);
+
+ n->removeType = OBJECT_FORMAT_CAST;
+ n->objects = list_make1(list_make2($6, $8));
+ n->behavior = $10;
+ n->missing_ok = $4;
+ $$ = (Node *) n;
+ }
+ ;
+
+
/*****************************************************************************
*
* QUERY:
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..5ecea23458f 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -37,6 +37,7 @@
#include "commands/event_trigger.h"
#include "commands/explain.h"
#include "commands/extension.h"
+#include "commands/formatcast.h"
#include "commands/lockcmds.h"
#include "commands/matview.h"
#include "commands/policy.h"
@@ -193,6 +194,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
case T_CreateTableAsStmt:
case T_CreateTableSpaceStmt:
case T_CreateTransformStmt:
+ case T_CreateFormatCastStmt:
case T_CreateTrigStmt:
case T_CreateUserMappingStmt:
case T_CreatedbStmt:
@@ -1755,6 +1757,10 @@ ProcessUtilitySlow(ParseState *pstate,
address = CreateTransform((CreateTransformStmt *) parsetree);
break;
+ case T_CreateFormatCastStmt:
+ address = CreateFormatCast((CreateFormatCastStmt *) parsetree);
+ break;
+
case T_AlterOpFamilyStmt:
AlterOpFamily((AlterOpFamilyStmt *) parsetree);
/* commands are stashed in AlterOpFamily */
@@ -2666,6 +2672,9 @@ CreateCommandTag(Node *parsetree)
case OBJECT_TRANSFORM:
tag = CMDTAG_DROP_TRANSFORM;
break;
+ case OBJECT_FORMAT_CAST:
+ tag = CMDTAG_DROP_FORMAT_CAST;
+ break;
case OBJECT_ACCESS_METHOD:
tag = CMDTAG_DROP_ACCESS_METHOD;
break;
@@ -2981,6 +2990,10 @@ CreateCommandTag(Node *parsetree)
tag = CMDTAG_CREATE_TRANSFORM;
break;
+ case T_CreateFormatCastStmt:
+ tag = CMDTAG_CREATE_FORMAT_CAST;
+ break;
+
case T_CreateTrigStmt:
tag = CMDTAG_CREATE_TRIGGER;
break;
@@ -3690,6 +3703,10 @@ GetCommandLogLevel(Node *parsetree)
lev = LOGSTMT_DDL;
break;
+ case T_CreateFormatCastStmt:
+ lev = LOGSTMT_DDL;
+ break;
+
case T_AlterOpFamilyStmt:
lev = LOGSTMT_DDL;
break;
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index dc98c5c5c09..54cf9c07f68 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -188,6 +188,9 @@ getSchemaData(Archive *fout, int *numTablesPtr)
pg_log_info("reading transforms");
getTransforms(fout);
+ pg_log_info("reading format casts");
+ getFormatCasts(fout);
+
pg_log_info("reading table inheritance information");
inhinfo = getInherits(fout, &numInherits);
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c56437d6057..6517a450daa 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -300,6 +300,7 @@ static void dumpProcLang(Archive *fout, const ProcLangInfo *plang);
static void dumpFunc(Archive *fout, const FuncInfo *finfo);
static void dumpCast(Archive *fout, const CastInfo *cast);
static void dumpTransform(Archive *fout, const TransformInfo *transform);
+static void dumpFormatCast(Archive *fout, const FormatCastInfo *formatcast);
static void dumpOpr(Archive *fout, const OprInfo *oprinfo);
static void dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo);
static void dumpOpclass(Archive *fout, const OpclassInfo *opcinfo);
@@ -9320,6 +9321,83 @@ getTransforms(Archive *fout)
destroyPQExpBuffer(query);
}
+/*
+ * getFormatCasts
+ * get basic information about every format cast in the system
+ */
+void
+getFormatCasts(Archive *fout)
+{
+ PGresult *res;
+ int ntups;
+ int i;
+ PQExpBuffer query;
+ FormatCastInfo *formatcastinfo;
+ int i_tableoid;
+ int i_oid;
+ int i_fmtsource;
+ int i_fmttarget;
+ int i_fmtfunc;
+
+ /* Format casts were introduced in v19 */
+ if (fout->remoteVersion < 190000)
+ return;
+
+ query = createPQExpBuffer();
+
+ appendPQExpBufferStr(query, "SELECT tableoid, oid, "
+ "fmtsource, fmttarget, fmtfunc "
+ "FROM pg_format_cast "
+ "ORDER BY 3,4");
+
+ res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+ ntups = PQntuples(res);
+
+ formatcastinfo = pg_malloc_array(FormatCastInfo, ntups);
+
+ i_tableoid = PQfnumber(res, "tableoid");
+ i_oid = PQfnumber(res, "oid");
+ i_fmtsource = PQfnumber(res, "fmtsource");
+ i_fmttarget = PQfnumber(res, "fmttarget");
+ i_fmtfunc = PQfnumber(res, "fmtfunc");
+
+ for (i = 0; i < ntups; i++)
+ {
+ PQExpBufferData namebuf;
+ TypeInfo *sTypeInfo;
+ TypeInfo *tTypeInfo;
+
+ formatcastinfo[i].dobj.objType = DO_FORMAT_CAST;
+ formatcastinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+ formatcastinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+ AssignDumpId(&formatcastinfo[i].dobj);
+ formatcastinfo[i].fmtsource = atooid(PQgetvalue(res, i, i_fmtsource));
+ formatcastinfo[i].fmttarget = atooid(PQgetvalue(res, i, i_fmttarget));
+ formatcastinfo[i].fmtfunc = atooid(PQgetvalue(res, i, i_fmtfunc));
+
+ /*
+ * Try to name the format cast as a concatenation of the type names.
+ * This is only used for purposes of sorting. If we fail to find
+ * either type, the name will be an empty string.
+ */
+ initPQExpBuffer(&namebuf);
+ sTypeInfo = findTypeByOid(formatcastinfo[i].fmtsource);
+ tTypeInfo = findTypeByOid(formatcastinfo[i].fmttarget);
+ if (sTypeInfo && tTypeInfo)
+ appendPQExpBuffer(&namebuf, "%s %s",
+ sTypeInfo->dobj.name, tTypeInfo->dobj.name);
+ formatcastinfo[i].dobj.name = namebuf.data;
+
+ /* Decide whether we want to dump it */
+ selectDumpableObject(&(formatcastinfo[i].dobj), fout);
+ }
+
+ PQclear(res);
+
+ destroyPQExpBuffer(query);
+}
+
/*
* getTableAttrs -
* for each interesting table, read info about its attributes
@@ -11913,6 +11991,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
case DO_TRANSFORM:
dumpTransform(fout, (const TransformInfo *) dobj);
break;
+ case DO_FORMAT_CAST:
+ dumpFormatCast(fout, (const FormatCastInfo *) dobj);
+ break;
case DO_SEQUENCE_SET:
dumpSequenceData(fout, (const TableDataInfo *) dobj);
break;
@@ -14255,6 +14336,90 @@ dumpTransform(Archive *fout, const TransformInfo *transform)
destroyPQExpBuffer(transformargs);
}
+/*
+ * Dump a format cast
+ */
+static void
+dumpFormatCast(Archive *fout, const FormatCastInfo *formatcast)
+{
+ DumpOptions *dopt = fout->dopt;
+ PQExpBuffer defqry;
+ PQExpBuffer delqry;
+ PQExpBuffer labelq;
+ PQExpBuffer formatcastargs;
+ FuncInfo *funcInfo;
+ const char *sourceType;
+ const char *targetType;
+
+ /* Do nothing if not dumping schema */
+ if (!dopt->dumpSchema)
+ return;
+
+ /* Cannot dump if we don't have the format cast function's info */
+ funcInfo = findFuncByOid(formatcast->fmtfunc);
+ if (funcInfo == NULL)
+ pg_fatal("could not find function definition for function with OID %u",
+ formatcast->fmtfunc);
+
+ defqry = createPQExpBuffer();
+ delqry = createPQExpBuffer();
+ labelq = createPQExpBuffer();
+ formatcastargs = createPQExpBuffer();
+
+ sourceType = getFormattedTypeName(fout, formatcast->fmtsource, zeroAsNone);
+ targetType = getFormattedTypeName(fout, formatcast->fmttarget, zeroAsNone);
+
+ appendPQExpBuffer(delqry, "DROP FORMAT CAST (%s AS %s);\n",
+ sourceType, targetType);
+
+ appendPQExpBuffer(defqry, "CREATE FORMAT CAST (%s AS %s) ",
+ sourceType, targetType);
+
+ {
+ char *fsig = format_function_signature(fout, funcInfo, true);
+
+ /*
+ * Always qualify the function name (format_function_signature won't
+ * qualify it).
+ */
+ appendPQExpBuffer(defqry, "WITH FUNCTION %s.%s",
+ fmtId(funcInfo->dobj.namespace->dobj.name), fsig);
+ free(fsig);
+ }
+ appendPQExpBufferStr(defqry, ";\n");
+
+ appendPQExpBuffer(labelq, "FORMAT CAST (%s AS %s)",
+ sourceType, targetType);
+
+ appendPQExpBuffer(formatcastargs, "(%s AS %s)",
+ sourceType, targetType);
+
+ if (dopt->binary_upgrade)
+ binary_upgrade_extension_member(defqry, &formatcast->dobj,
+ "FORMAT CAST", formatcastargs->data, NULL);
+
+ if (formatcast->dobj.dump & DUMP_COMPONENT_DEFINITION)
+ ArchiveEntry(fout, formatcast->dobj.catId, formatcast->dobj.dumpId,
+ ARCHIVE_OPTS(.tag = labelq->data,
+ .description = "FORMAT CAST",
+ .section = SECTION_PRE_DATA,
+ .createStmt = defqry->data,
+ .dropStmt = delqry->data,
+ .deps = formatcast->dobj.dependencies,
+ .nDeps = formatcast->dobj.nDeps));
+
+ /* Dump Format cast Comments */
+ if (formatcast->dobj.dump & DUMP_COMPONENT_COMMENT)
+ dumpComment(fout, "FORMAT CAST", formatcastargs->data,
+ NULL, "",
+ formatcast->dobj.catId, 0, formatcast->dobj.dumpId);
+
+ destroyPQExpBuffer(defqry);
+ destroyPQExpBuffer(delqry);
+ destroyPQExpBuffer(labelq);
+ destroyPQExpBuffer(formatcastargs);
+}
+
/*
* dumpOpr
@@ -20696,6 +20861,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
case DO_FDW:
case DO_FOREIGN_SERVER:
case DO_TRANSFORM:
+ case DO_FORMAT_CAST:
/* Pre-data objects: must come before the pre-data boundary */
addObjectDependency(preDataBound, dobj->dumpId);
break;
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 5a6726d8b12..c767185ec15 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -73,6 +73,7 @@ typedef enum
DO_FOREIGN_SERVER,
DO_DEFAULT_ACL,
DO_TRANSFORM,
+ DO_FORMAT_CAST,
DO_LARGE_OBJECT,
DO_LARGE_OBJECT_DATA,
DO_PRE_DATA_BOUNDARY,
@@ -559,6 +560,14 @@ typedef struct _transformInfo
Oid trftosql;
} TransformInfo;
+typedef struct _formatCastInfo
+{
+ DumpableObject dobj;
+ Oid fmtsource;
+ Oid fmttarget;
+ Oid fmtfunc;
+} FormatCastInfo;
+
/* InhInfo isn't a DumpableObject, just temporary state */
typedef struct _inhInfo
{
@@ -814,6 +823,7 @@ extern void getTriggers(Archive *fout, TableInfo tblinfo[], int numTables);
extern void getProcLangs(Archive *fout);
extern void getCasts(Archive *fout);
extern void getTransforms(Archive *fout);
+extern void getFormatCasts(Archive *fout);
extern void getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables);
extern bool shouldPrintColumn(const DumpOptions *dopt, const TableInfo *tbinfo, int colno);
extern void getTSParsers(Archive *fout);
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 03e5c1c1116..3603f5f5fe3 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -60,6 +60,7 @@ enum dbObjectTypePriorities
PRIO_EXTENSION,
PRIO_TYPE, /* used for DO_TYPE and DO_SHELL_TYPE */
PRIO_CAST,
+ PRIO_FORMAT_CAST,
PRIO_FUNC,
PRIO_AGG,
PRIO_ACCESS_METHOD,
@@ -128,6 +129,7 @@ static const int dbObjectTypePriority[] =
[DO_FK_CONSTRAINT] = PRIO_FK_CONSTRAINT,
[DO_PROCLANG] = PRIO_PROCLANG,
[DO_CAST] = PRIO_CAST,
+ [DO_FORMAT_CAST] = PRIO_FORMAT_CAST,
[DO_TABLE_DATA] = PRIO_TABLE_DATA,
[DO_SEQUENCE_SET] = PRIO_SEQUENCE_SET,
[DO_DUMMY_TYPE] = PRIO_DUMMY_TYPE,
@@ -1649,6 +1651,13 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
((CastInfo *) obj)->casttarget,
obj->dumpId, obj->catId.oid);
return;
+ case DO_FORMAT_CAST:
+ snprintf(buf, bufsize,
+ "FORMAT CAST %u to %u (ID %d OID %u)",
+ ((FormatCastInfo *) obj)->fmtsource,
+ ((FormatCastInfo *) obj)->fmttarget,
+ obj->dumpId, obj->catId.oid);
+ return;
case DO_TRANSFORM:
snprintf(buf, bufsize,
"TRANSFORM %u lang %u (ID %d OID %u)",
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index bab57372b88..8397cf0756a 100644
--- a/src/include/catalog/Makefile
+++ b/src/include/catalog/Makefile
@@ -75,6 +75,7 @@ CATALOG_HEADERS := \
pg_parameter_acl.h \
pg_partitioned_table.h \
pg_range.h \
+ pg_format_cast.h \
pg_transform.h \
pg_sequence.h \
pg_publication.h \
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 635c0d9cb13..6402cc76c87 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
*/
/* yyyymmddN */
-#define CATALOG_VERSION_NO 202606281
+#define CATALOG_VERSION_NO 202606282
#endif
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index fa836e4ee25..3cb63f85dd2 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -62,6 +62,7 @@ catalog_headers = [
'pg_parameter_acl.h',
'pg_partitioned_table.h',
'pg_range.h',
+ 'pg_format_cast.h',
'pg_transform.h',
'pg_sequence.h',
'pg_publication.h',
diff --git a/src/include/catalog/pg_format_cast.h b/src/include/catalog/pg_format_cast.h
new file mode 100644
index 00000000000..f4647d82aa0
--- /dev/null
+++ b/src/include/catalog/pg_format_cast.h
@@ -0,0 +1,62 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_format_cast.h
+ * definition of the "format cast" system catalog (pg_format_cast)
+ *
+ * A format cast registers a function that implements
+ * CAST(expr AS target FORMAT format_expr) for a particular
+ * (source type, target type) pair. The format cast function receives the
+ * source value and the FORMAT expression (as text) and returns the target
+ * type. This is intentionally separate from pg_cast: ordinary casts have
+ * implicit/assignment/explicit contexts and binary-coercible/WITH INOUT/
+ * WITHOUT FUNCTION methods, whereas a formatted cast is always explicit and
+ * always requires a function.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_format_cast.h
+ *
+ * NOTES
+ * The Catalog.pm module reads this file and derives schema
+ * information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_FORMAT_CAST_H
+#define PG_FORMAT_CAST_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_format_cast_d.h" /* IWYU pragma: export */
+
+/* ----------------
+ * pg_format_cast definition. cpp turns this into
+ * typedef struct FormData_pg_format_cast
+ * ----------------
+ */
+BEGIN_CATALOG_STRUCT
+
+CATALOG(pg_format_cast,8647,FormatCastRelationId)
+{
+ Oid oid; /* oid */
+ Oid fmtsource BKI_LOOKUP(pg_type); /* source type */
+ Oid fmttarget BKI_LOOKUP(pg_type); /* target type */
+ Oid fmtfunc BKI_LOOKUP(pg_proc); /* format cast function */
+} FormData_pg_format_cast;
+
+END_CATALOG_STRUCT
+
+/* ----------------
+ * Form_pg_format_cast corresponds to a pointer to a tuple with
+ * the format of pg_format_cast relation.
+ * ----------------
+ */
+typedef FormData_pg_format_cast *Form_pg_format_cast;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_format_cast_oid_index, 8648, FormatCastOidIndexId, pg_format_cast, btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_format_cast_source_target_index, 8649, FormatCastSourceTargetIndexId, pg_format_cast, btree(fmtsource oid_ops, fmttarget oid_ops));
+
+MAKE_SYSCACHE(FORMATCASTOID, pg_format_cast_oid_index, 8);
+MAKE_SYSCACHE(FORMATCASTSOURCETARGET, pg_format_cast_source_target_index, 8);
+
+#endif /* PG_FORMAT_CAST_H */
diff --git a/src/include/commands/formatcast.h b/src/include/commands/formatcast.h
new file mode 100644
index 00000000000..8ca84a3b8df
--- /dev/null
+++ b/src/include/commands/formatcast.h
@@ -0,0 +1,24 @@
+/*-------------------------------------------------------------------------
+ *
+ * formatcast.h
+ * prototypes for formatcastcmds.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/formatcast.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef FORMATCAST_H
+#define FORMATCAST_H
+
+#include "catalog/objectaddress.h"
+#include "nodes/parsenodes.h"
+
+extern ObjectAddress CreateFormatCast(CreateFormatCastStmt *stmt);
+extern Oid get_format_cast_oid(Oid sourcetypeid, Oid targettypeid,
+ bool missing_ok);
+
+#endif /* FORMATCAST_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 759c6bfae54..71374dab22e 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2445,6 +2445,7 @@ typedef enum ObjectType
OBJECT_FDW,
OBJECT_FOREIGN_SERVER,
OBJECT_FOREIGN_TABLE,
+ OBJECT_FORMAT_CAST,
OBJECT_FUNCTION,
OBJECT_INDEX,
OBJECT_LANGUAGE,
@@ -4356,6 +4357,22 @@ typedef struct CreateTransformStmt
ObjectWithArgs *tosql;
} CreateTransformStmt;
+/* ----------------------
+ * CREATE FORMAT CAST Statement
+ *
+ * Registers a format cast function for CAST(... AS target FORMAT ...) on a
+ * (sourcetype, targettype) pair. DROP FORMAT CAST reuses the generic
+ * DropStmt path with removeType == OBJECT_FORMAT_CAST.
+ * ----------------------
+ */
+typedef struct CreateFormatCastStmt
+{
+ NodeTag type;
+ TypeName *sourcetype; /* source data type */
+ TypeName *targettype; /* target data type */
+ ObjectWithArgs *func; /* format cast function */
+} CreateFormatCastStmt;
+
/* ----------------------
* PREPARE Statement
* ----------------------
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index befae5f6b4f..c1dda8ab959 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -95,6 +95,7 @@ PG_CMDTAG(CMDTAG_CREATE_EVENT_TRIGGER, "CREATE EVENT TRIGGER", false, false, fal
PG_CMDTAG(CMDTAG_CREATE_EXTENSION, "CREATE EXTENSION", true, false, false)
PG_CMDTAG(CMDTAG_CREATE_FOREIGN_DATA_WRAPPER, "CREATE FOREIGN DATA WRAPPER", true, false, false)
PG_CMDTAG(CMDTAG_CREATE_FOREIGN_TABLE, "CREATE FOREIGN TABLE", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_FORMAT_CAST, "CREATE FORMAT CAST", true, false, false)
PG_CMDTAG(CMDTAG_CREATE_FUNCTION, "CREATE FUNCTION", true, false, false)
PG_CMDTAG(CMDTAG_CREATE_INDEX, "CREATE INDEX", true, false, false)
PG_CMDTAG(CMDTAG_CREATE_LANGUAGE, "CREATE LANGUAGE", true, false, false)
@@ -148,6 +149,7 @@ PG_CMDTAG(CMDTAG_DROP_EVENT_TRIGGER, "DROP EVENT TRIGGER", false, false, false)
PG_CMDTAG(CMDTAG_DROP_EXTENSION, "DROP EXTENSION", true, false, false)
PG_CMDTAG(CMDTAG_DROP_FOREIGN_DATA_WRAPPER, "DROP FOREIGN DATA WRAPPER", true, false, false)
PG_CMDTAG(CMDTAG_DROP_FOREIGN_TABLE, "DROP FOREIGN TABLE", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_FORMAT_CAST, "DROP FORMAT CAST", true, false, false)
PG_CMDTAG(CMDTAG_DROP_FUNCTION, "DROP FUNCTION", true, false, false)
PG_CMDTAG(CMDTAG_DROP_INDEX, "DROP INDEX", true, false, false)
PG_CMDTAG(CMDTAG_DROP_LANGUAGE, "DROP LANGUAGE", true, false, false)
diff --git a/src/test/regress/expected/format_casts.out b/src/test/regress/expected/format_casts.out
new file mode 100644
index 00000000000..2ae4ca0d0d0
--- /dev/null
+++ b/src/test/regress/expected/format_casts.out
@@ -0,0 +1,156 @@
+--
+-- FORMAT CASTS
+--
+-- CREATE/DROP FORMAT CAST registers format cast metadata in pg_format_cast,
+-- keyed by a (source type, target type) pair. This is catalog and DDL
+-- infrastructure only; it does not transform or execute formatted casts.
+-- A simple format cast function with the required signature
+-- function(source_type, text) returns target_type
+CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2;
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION int4_to_text_fmt(integer, text);
+-- It shows up in the catalog (use regtype/regprocedure, not raw OIDs)
+SELECT fmtsource::regtype, fmttarget::regtype, fmtfunc::regprocedure
+ FROM pg_format_cast
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype;
+ fmtsource | fmttarget | fmtfunc
+-----------+-----------+--------------------------------
+ integer | text | int4_to_text_fmt(integer,text)
+(1 row)
+
+-- ... and as a first-class object
+SELECT pg_describe_object('pg_format_cast'::regclass, oid, 0)
+ FROM pg_format_cast
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype;
+ pg_describe_object
+----------------------------------
+ format cast from integer to text
+(1 row)
+
+-- Object identity and COMMENT use "FORMAT CAST (source AS target)" (no "FOR CAST").
+SELECT identity FROM pg_identify_object('pg_format_cast'::regclass,
+ (SELECT oid FROM pg_format_cast
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype), 0);
+ identity
+------------------------------
+ (integer AS pg_catalog.text)
+(1 row)
+
+COMMENT ON FORMAT CAST (integer AS text) IS 'demo format cast';
+SELECT obj_description((SELECT oid FROM pg_format_cast
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype),
+ 'pg_format_cast');
+ obj_description
+------------------
+ demo format cast
+(1 row)
+
+COMMENT ON FORMAT CAST (integer AS text) IS NULL;
+-- pg_identify_object_as_address() must round-trip back through
+-- pg_get_object_address() to the same catalog object.
+WITH fmt AS (
+ SELECT oid FROM pg_format_cast
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype
+), addr AS (
+ SELECT * FROM pg_identify_object_as_address('pg_format_cast'::regclass,
+ (SELECT oid FROM fmt), 0)
+)
+SELECT addr.type, addr.object_names, addr.object_args,
+ (SELECT classid = 'pg_format_cast'::regclass
+ AND objid = (SELECT oid FROM fmt)
+ AND objsubid = 0
+ FROM pg_get_object_address(addr.type, addr.object_names, addr.object_args))
+ AS round_trips
+ FROM addr;
+ type | object_names | object_args | round_trips
+-------------+--------------+-------------------+-------------
+ format cast | {integer} | {pg_catalog.text} | t
+(1 row)
+
+-- Only one format cast per (source, target) pair
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION int4_to_text_fmt(integer, text);
+ERROR: format cast from type integer to type text already exists
+-- Function signature validation
+CREATE FUNCTION fmt_bad_nargs(integer) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text;
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION fmt_bad_nargs(integer); -- wrong # of arguments
+ERROR: format cast function must take exactly two arguments
+CREATE FUNCTION fmt_bad_arg2(integer, integer) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text;
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION fmt_bad_arg2(integer, integer); -- second arg not text
+ERROR: second argument of format cast function must be type text
+CREATE FUNCTION fmt_bad_arg1(bigint, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text;
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION fmt_bad_arg1(bigint, text); -- first arg mismatch
+ERROR: first argument of format cast function must be type integer
+CREATE FUNCTION fmt_bad_ret(integer, text) RETURNS boolean
+ LANGUAGE sql IMMUTABLE RETURN true;
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION fmt_bad_ret(integer, text); -- return type mismatch
+ERROR: return data type of format cast function must be type text
+CREATE FUNCTION fmt_bad_set(integer, text) RETURNS SETOF text
+ LANGUAGE sql IMMUTABLE AS $$ SELECT $1::text $$;
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION fmt_bad_set(integer, text); -- set-returning rejected
+ERROR: format cast function must not return a set
+-- No pseudo-types
+CREATE FUNCTION fmt_anyel(anyelement, text) RETURNS text
+ LANGUAGE sql IMMUTABLE AS $$ SELECT $2 $$;
+CREATE FORMAT CAST (anyelement AS text)
+ WITH FUNCTION fmt_anyel(anyelement, text);
+ERROR: source data type anyelement is a pseudo-type
+-- Registering a format cast does not enable a formatted cast: the FORMAT
+-- clause must not be silently ignored or rewritten to a built-in function,
+-- so CAST(... FORMAT ...) is rejected during parse analysis.
+SELECT CAST(5 AS text FORMAT 'YYYY');
+ERROR: formatted casts are not implemented yet
+LINE 1: SELECT CAST(5 AS text FORMAT 'YYYY');
+ ^
+DETAIL: No format cast resolution mechanism is available.
+-- Dependency behavior: the format cast depends on its function.
+DROP FUNCTION int4_to_text_fmt(integer, text); -- fails (RESTRICT)
+ERROR: cannot drop function int4_to_text_fmt(integer,text) because other objects depend on it
+DETAIL: format cast from integer to text depends on function int4_to_text_fmt(integer,text)
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+DROP FUNCTION int4_to_text_fmt(integer, text) CASCADE; -- drops format cast
+NOTICE: drop cascades to format cast from integer to text
+SELECT count(*) FROM pg_format_cast
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype;
+ count
+-------
+ 0
+(1 row)
+
+-- Privileges: the creator must own the source or the target type. (The
+-- ownership check happens before the function is looked up, so the function
+-- name here is irrelevant.)
+CREATE ROLE regress_format_cast_user;
+SET ROLE regress_format_cast_user;
+CREATE FORMAT CAST (text AS integer)
+ WITH FUNCTION pg_catalog.length(text);
+ERROR: must be owner of type text or type integer
+RESET ROLE;
+DROP ROLE regress_format_cast_user;
+-- DROP FORMAT CAST, including IF EXISTS
+CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2;
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION int4_to_text_fmt(integer, text);
+DROP FORMAT CAST (integer AS text);
+DROP FORMAT CAST (integer AS text); -- fails, gone
+ERROR: format cast from type integer to type text does not exist
+DROP FORMAT CAST IF EXISTS (integer AS text); -- notice, no error
+NOTICE: format cast from type integer to type text does not exist, skipping
+-- Clean up
+DROP FUNCTION int4_to_text_fmt(integer, text);
+DROP FUNCTION fmt_bad_nargs(integer);
+DROP FUNCTION fmt_bad_arg2(integer, integer);
+DROP FUNCTION fmt_bad_arg1(bigint, text);
+DROP FUNCTION fmt_bad_ret(integer, text);
+DROP FUNCTION fmt_bad_set(integer, text);
+DROP FUNCTION fmt_anyel(anyelement, text);
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index d64169b7bf0..cfe3e39770e 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -257,6 +257,9 @@ NOTICE: checking pg_range {rngmltconstruct1} => pg_proc {oid}
NOTICE: checking pg_range {rngmltconstruct2} => pg_proc {oid}
NOTICE: checking pg_range {rngcanonical} => pg_proc {oid}
NOTICE: checking pg_range {rngsubdiff} => pg_proc {oid}
+NOTICE: checking pg_format_cast {fmtsource} => pg_type {oid}
+NOTICE: checking pg_format_cast {fmttarget} => pg_type {oid}
+NOTICE: checking pg_format_cast {fmtfunc} => pg_proc {oid}
NOTICE: checking pg_transform {trftype} => pg_type {oid}
NOTICE: checking pg_transform {trflang} => pg_language {oid}
NOTICE: checking pg_transform {trffromsql} => pg_proc {oid}
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..f1bf8f6504b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi
# ----------
# Another group of parallel tests
# ----------
-test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse create_property_graph for_portion_of
+test: create_aggregate create_function_sql create_cast format_casts constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse create_property_graph for_portion_of
# ----------
# sanity_check does a vacuum, affecting the sort order of SELECT *
diff --git a/src/test/regress/sql/format_casts.sql b/src/test/regress/sql/format_casts.sql
new file mode 100644
index 00000000000..4453a09784e
--- /dev/null
+++ b/src/test/regress/sql/format_casts.sql
@@ -0,0 +1,126 @@
+--
+-- FORMAT CASTS
+--
+-- CREATE/DROP FORMAT CAST registers format cast metadata in pg_format_cast,
+-- keyed by a (source type, target type) pair. This is catalog and DDL
+-- infrastructure only; it does not transform or execute formatted casts.
+
+-- A simple format cast function with the required signature
+-- function(source_type, text) returns target_type
+CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2;
+
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION int4_to_text_fmt(integer, text);
+
+-- It shows up in the catalog (use regtype/regprocedure, not raw OIDs)
+SELECT fmtsource::regtype, fmttarget::regtype, fmtfunc::regprocedure
+ FROM pg_format_cast
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype;
+
+-- ... and as a first-class object
+SELECT pg_describe_object('pg_format_cast'::regclass, oid, 0)
+ FROM pg_format_cast
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype;
+
+-- Object identity and COMMENT use "FORMAT CAST (source AS target)" (no "FOR CAST").
+SELECT identity FROM pg_identify_object('pg_format_cast'::regclass,
+ (SELECT oid FROM pg_format_cast
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype), 0);
+COMMENT ON FORMAT CAST (integer AS text) IS 'demo format cast';
+SELECT obj_description((SELECT oid FROM pg_format_cast
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype),
+ 'pg_format_cast');
+COMMENT ON FORMAT CAST (integer AS text) IS NULL;
+
+-- pg_identify_object_as_address() must round-trip back through
+-- pg_get_object_address() to the same catalog object.
+WITH fmt AS (
+ SELECT oid FROM pg_format_cast
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype
+), addr AS (
+ SELECT * FROM pg_identify_object_as_address('pg_format_cast'::regclass,
+ (SELECT oid FROM fmt), 0)
+)
+SELECT addr.type, addr.object_names, addr.object_args,
+ (SELECT classid = 'pg_format_cast'::regclass
+ AND objid = (SELECT oid FROM fmt)
+ AND objsubid = 0
+ FROM pg_get_object_address(addr.type, addr.object_names, addr.object_args))
+ AS round_trips
+ FROM addr;
+
+-- Only one format cast per (source, target) pair
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION int4_to_text_fmt(integer, text);
+
+-- Function signature validation
+CREATE FUNCTION fmt_bad_nargs(integer) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text;
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION fmt_bad_nargs(integer); -- wrong # of arguments
+
+CREATE FUNCTION fmt_bad_arg2(integer, integer) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text;
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION fmt_bad_arg2(integer, integer); -- second arg not text
+
+CREATE FUNCTION fmt_bad_arg1(bigint, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text;
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION fmt_bad_arg1(bigint, text); -- first arg mismatch
+
+CREATE FUNCTION fmt_bad_ret(integer, text) RETURNS boolean
+ LANGUAGE sql IMMUTABLE RETURN true;
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION fmt_bad_ret(integer, text); -- return type mismatch
+
+CREATE FUNCTION fmt_bad_set(integer, text) RETURNS SETOF text
+ LANGUAGE sql IMMUTABLE AS $$ SELECT $1::text $$;
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION fmt_bad_set(integer, text); -- set-returning rejected
+
+-- No pseudo-types
+CREATE FUNCTION fmt_anyel(anyelement, text) RETURNS text
+ LANGUAGE sql IMMUTABLE AS $$ SELECT $2 $$;
+CREATE FORMAT CAST (anyelement AS text)
+ WITH FUNCTION fmt_anyel(anyelement, text);
+
+-- Registering a format cast does not enable a formatted cast: the FORMAT
+-- clause must not be silently ignored or rewritten to a built-in function,
+-- so CAST(... FORMAT ...) is rejected during parse analysis.
+SELECT CAST(5 AS text FORMAT 'YYYY');
+
+-- Dependency behavior: the format cast depends on its function.
+DROP FUNCTION int4_to_text_fmt(integer, text); -- fails (RESTRICT)
+DROP FUNCTION int4_to_text_fmt(integer, text) CASCADE; -- drops format cast
+SELECT count(*) FROM pg_format_cast
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype;
+
+-- Privileges: the creator must own the source or the target type. (The
+-- ownership check happens before the function is looked up, so the function
+-- name here is irrelevant.)
+CREATE ROLE regress_format_cast_user;
+SET ROLE regress_format_cast_user;
+CREATE FORMAT CAST (text AS integer)
+ WITH FUNCTION pg_catalog.length(text);
+RESET ROLE;
+DROP ROLE regress_format_cast_user;
+
+-- DROP FORMAT CAST, including IF EXISTS
+CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2;
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION int4_to_text_fmt(integer, text);
+DROP FORMAT CAST (integer AS text);
+DROP FORMAT CAST (integer AS text); -- fails, gone
+DROP FORMAT CAST IF EXISTS (integer AS text); -- notice, no error
+
+-- Clean up
+DROP FUNCTION int4_to_text_fmt(integer, text);
+DROP FUNCTION fmt_bad_nargs(integer);
+DROP FUNCTION fmt_bad_arg2(integer, integer);
+DROP FUNCTION fmt_bad_arg1(bigint, text);
+DROP FUNCTION fmt_bad_ret(integer, text);
+DROP FUNCTION fmt_bad_set(integer, text);
+DROP FUNCTION fmt_anyel(anyelement, text);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index c5db6ca6705..b8f50855e18 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -575,6 +575,7 @@ CreateExtensionStmt
CreateFdwStmt
CreateForeignServerStmt
CreateForeignTableStmt
+CreateFormatCastStmt
CreateFunctionStmt
CreateOpClassItem
CreateOpClassStmt
@@ -921,6 +922,7 @@ FormData_pg_extension
FormData_pg_foreign_data_wrapper
FormData_pg_foreign_server
FormData_pg_foreign_table
+FormData_pg_format_cast
FormData_pg_index
FormData_pg_inherits
FormData_pg_language
@@ -986,6 +988,7 @@ Form_pg_extension
Form_pg_foreign_data_wrapper
Form_pg_foreign_server
Form_pg_foreign_table
+Form_pg_format_cast
Form_pg_index
Form_pg_inherits
Form_pg_language
@@ -1027,6 +1030,7 @@ Form_pg_ts_parser
Form_pg_ts_template
Form_pg_type
Form_pg_user_mapping
+FormatCastInfo
FormatNode
FreeBlockNumberArray
FreeListData
--
2.54.0
[application/octet-stream] v2-0003-Resolve-CAST-FORMAT-with-CoerceViaFormatCast.patch (44.5K, ../../CABXr29HF+AHV0FNxQfHyN-ByW6-3+pTBe5Pxm23wRpBYOmfohA@mail.gmail.com/4-v2-0003-Resolve-CAST-FORMAT-with-CoerceViaFormatCast.patch)
download | inline diff:
From fa11e0046abfb518b65b0916c33e66404bef3492 Mon Sep 17 00:00:00 2001
From: Haibo Yan <[email protected]>
Date: Wed, 24 Jun 2026 20:46:00 -0700
Subject: [PATCH 3/4] Resolve CAST ... FORMAT with CoerceViaFormatCast
Resolve formatted casts through pg_format_cast during parse analysis. The
FORMAT expression is transformed and coerced to text, unknown source
literals are treated as text, and format cast lookup is performed by exact
source and target type. If no format cast exists, the cast fails; a FORMAT
clause never falls back to ordinary cast resolution.
Represent the analyzed expression with a CoerceViaFormatCast node. The
node preserves CAST ... FORMAT syntax for ruleutils and pg_dump, and
records a dependency on the pg_format_cast row as well as on the format cast
function. Execution still uses the ordinary function-call machinery via
ExecInitFunc, so no new executor opcode or JIT support is needed.
Target typmods and domain checks are enforced by the existing coercion
machinery layered above the node.
---
src/backend/catalog/dependency.c | 23 ++
src/backend/executor/execExpr.c | 18 ++
src/backend/nodes/nodeFuncs.c | 53 +++++
src/backend/parser/parse_expr.c | 136 ++++++++++-
src/backend/utils/adt/ruleutils.c | 19 ++
src/backend/utils/cache/lsyscache.c | 37 +++
src/include/nodes/primnodes.h | 38 +++
src/include/utils/lsyscache.h | 2 +
src/test/regress/expected/expressions.out | 30 ++-
src/test/regress/expected/format_casts.out | 263 ++++++++++++++++++++-
src/test/regress/sql/expressions.sql | 14 +-
src/test/regress/sql/format_casts.sql | 153 +++++++++++-
12 files changed, 742 insertions(+), 44 deletions(-)
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index ff7ab606bcc..34be165849a 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -2131,6 +2131,29 @@ find_expr_references_walker(Node *node,
add_object_address(CollationRelationId, iocoerce->resultcollid, 0,
context->addrs);
}
+ else if (IsA(node, CoerceViaFormatCast))
+ {
+ CoerceViaFormatCast *fmt = (CoerceViaFormatCast *) node;
+
+ /* depend on the result type */
+ add_object_address(TypeRelationId, fmt->resulttype, 0,
+ context->addrs);
+ /* depend on the format cast function */
+ add_object_address(ProcedureRelationId, fmt->formatfunc, 0,
+ context->addrs);
+ /*
+ * Also depend on the pg_format_cast row itself, so that DROP FORMAT CAST
+ * is refused (or cascades) while a stored expression uses it.
+ */
+ if (OidIsValid(fmt->formatcastid))
+ add_object_address(FormatCastRelationId, fmt->formatcastid, 0,
+ context->addrs);
+ /* the collation might not be referenced anywhere else, either */
+ if (OidIsValid(fmt->resultcollid) &&
+ fmt->resultcollid != DEFAULT_COLLATION_OID)
+ add_object_address(CollationRelationId, fmt->resultcollid, 0,
+ context->addrs);
+ }
else if (IsA(node, ArrayCoerceExpr))
{
ArrayCoerceExpr *acoerce = (ArrayCoerceExpr *) node;
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index cfea7e160c2..bc9c11c2a45 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1200,6 +1200,24 @@ ExecInitExprRec(Expr *node, ExprState *state,
break;
}
+ case T_CoerceViaFormatCast:
+ {
+ CoerceViaFormatCast *fmt = (CoerceViaFormatCast *) node;
+
+ /*
+ * A formatted cast is executed exactly like a call to its
+ * format cast function: formatfunc(arg, format). Reuse the
+ * ordinary function-call setup, which also performs the
+ * run-time EXECUTE permission check on the format cast function.
+ */
+ ExecInitFunc(&scratch, node,
+ list_make2(fmt->arg, fmt->format),
+ fmt->formatfunc, fmt->inputcollid,
+ state);
+ ExprEvalPushStep(state, &scratch);
+ break;
+ }
+
case T_OpExpr:
{
OpExpr *op = (OpExpr *) node;
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 66495546179..28fe0814732 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -182,6 +182,9 @@ exprType(const Node *expr)
case T_CoerceViaIO:
type = ((const CoerceViaIO *) expr)->resulttype;
break;
+ case T_CoerceViaFormatCast:
+ type = ((const CoerceViaFormatCast *) expr)->resulttype;
+ break;
case T_ArrayCoerceExpr:
type = ((const ArrayCoerceExpr *) expr)->resulttype;
break;
@@ -531,6 +534,8 @@ exprTypmod(const Node *expr)
break;
case T_CoerceToDomain:
return ((const CoerceToDomain *) expr)->resulttypmod;
+ case T_CoerceViaFormatCast:
+ return ((const CoerceViaFormatCast *) expr)->resulttypmod;
case T_CoerceToDomainValue:
return ((const CoerceToDomainValue *) expr)->typeMod;
case T_SetToDefault:
@@ -943,6 +948,9 @@ exprCollation(const Node *expr)
case T_CoerceViaIO:
coll = ((const CoerceViaIO *) expr)->resultcollid;
break;
+ case T_CoerceViaFormatCast:
+ coll = ((const CoerceViaFormatCast *) expr)->resultcollid;
+ break;
case T_ArrayCoerceExpr:
coll = ((const ArrayCoerceExpr *) expr)->resultcollid;
break;
@@ -1227,6 +1235,9 @@ exprSetCollation(Node *expr, Oid collation)
case T_CoerceViaIO:
((CoerceViaIO *) expr)->resultcollid = collation;
break;
+ case T_CoerceViaFormatCast:
+ ((CoerceViaFormatCast *) expr)->resultcollid = collation;
+ break;
case T_ArrayCoerceExpr:
((ArrayCoerceExpr *) expr)->resultcollid = collation;
break;
@@ -1349,6 +1360,9 @@ exprSetInputCollation(Node *expr, Oid inputcollation)
case T_FuncExpr:
((FuncExpr *) expr)->inputcollid = inputcollation;
break;
+ case T_CoerceViaFormatCast:
+ ((CoerceViaFormatCast *) expr)->inputcollid = inputcollation;
+ break;
case T_OpExpr:
((OpExpr *) expr)->inputcollid = inputcollation;
break;
@@ -1527,6 +1541,15 @@ exprLocation(const Node *expr)
exprLocation((Node *) cexpr->arg));
}
break;
+ case T_CoerceViaFormatCast:
+ {
+ const CoerceViaFormatCast *cexpr = (const CoerceViaFormatCast *) expr;
+
+ /* Much as above */
+ loc = leftmostLoc(cexpr->location,
+ exprLocation((Node *) cexpr->arg));
+ }
+ break;
case T_ArrayCoerceExpr:
{
const ArrayCoerceExpr *cexpr = (const ArrayCoerceExpr *) expr;
@@ -1994,6 +2017,15 @@ check_functions_in_node(Node *node, check_function_callback checker,
return true;
}
break;
+ case T_CoerceViaFormatCast:
+ {
+ CoerceViaFormatCast *expr = (CoerceViaFormatCast *) node;
+
+ /* check the format cast function */
+ if (checker(expr->formatfunc, context))
+ return true;
+ }
+ break;
case T_RowCompareExpr:
{
RowCompareExpr *rcexpr = (RowCompareExpr *) node;
@@ -2296,6 +2328,16 @@ expression_tree_walker_impl(Node *node,
return WALK(((RelabelType *) node)->arg);
case T_CoerceViaIO:
return WALK(((CoerceViaIO *) node)->arg);
+ case T_CoerceViaFormatCast:
+ {
+ CoerceViaFormatCast *fmt = (CoerceViaFormatCast *) node;
+
+ if (WALK(fmt->arg))
+ return true;
+ if (WALK(fmt->format))
+ return true;
+ }
+ break;
case T_ArrayCoerceExpr:
{
ArrayCoerceExpr *acoerce = (ArrayCoerceExpr *) node;
@@ -3318,6 +3360,17 @@ expression_tree_mutator_impl(Node *node,
return (Node *) newnode;
}
break;
+ case T_CoerceViaFormatCast:
+ {
+ CoerceViaFormatCast *fmtcoerce = (CoerceViaFormatCast *) node;
+ CoerceViaFormatCast *newnode;
+
+ FLATCOPY(newnode, fmtcoerce, CoerceViaFormatCast);
+ MUTATE(newnode->arg, fmtcoerce->arg, Expr *);
+ MUTATE(newnode->format, fmtcoerce->format, Expr *);
+ return (Node *) newnode;
+ }
+ break;
case T_ArrayCoerceExpr:
{
ArrayCoerceExpr *acoerce = (ArrayCoerceExpr *) node;
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index ba6480acf12..3d481119d16 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -78,6 +78,7 @@ static Node *transformWholeRowRef(ParseState *pstate,
int sublevels_up, int location);
static Node *transformIndirection(ParseState *pstate, A_Indirection *ind);
static Node *transformTypeCast(ParseState *pstate, TypeCast *tc);
+static Node *transformFormattedTypeCast(ParseState *pstate, TypeCast *tc);
static Node *transformCollateClause(ParseState *pstate, CollateClause *c);
static Node *transformJsonObjectConstructor(ParseState *pstate,
JsonObjectConstructor *ctor);
@@ -2744,17 +2745,12 @@ transformTypeCast(ParseState *pstate, TypeCast *tc)
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.
+ * A FORMAT clause turns this into a formatted cast, which is resolved
+ * exclusively through the pg_format_cast catalog (never through ordinary
+ * cast rules). Handle it in a separate code path.
*/
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))));
+ return transformFormattedTypeCast(pstate, tc);
/* Look up the type name first */
typenameTypeIdAndMod(pstate, tc->typeName, &targetType, &targetTypmod);
@@ -2824,6 +2820,128 @@ transformTypeCast(ParseState *pstate, TypeCast *tc)
return result;
}
+/*
+ * Handle a formatted cast: CAST(arg AS typeName FORMAT format).
+ *
+ * Resolved exclusively through the pg_format_cast catalog, keyed by
+ * (source type, target type). We build a CoerceViaFormatCast node, which at
+ * execution time calls the registered format cast function with the source
+ * value and the FORMAT expression (coerced to text). Using a dedicated node
+ * (rather than a bare FuncExpr) lets the expression deparse back to
+ * CAST ... FORMAT and depend on the pg_format_cast row.
+ */
+static Node *
+transformFormattedTypeCast(ParseState *pstate, TypeCast *tc)
+{
+ Node *expr;
+ Node *fmt;
+ Oid sourceType;
+ Oid targetType;
+ int32 targetTypmod;
+ Oid formatcastid;
+ Oid fmtfuncid = InvalidOid;
+ Oid funcrettype;
+ CoerceViaFormatCast *cvf;
+ Node *result;
+ int location;
+
+ /* Resolve the declared target type, exactly as an ordinary cast does. */
+ typenameTypeIdAndMod(pstate, tc->typeName, &targetType, &targetTypmod);
+
+ /* Transform the source expression. */
+ expr = transformExprRecurse(pstate, tc->arg);
+ sourceType = exprType(expr);
+
+ /*
+ * An unknown-type source (typically a bare string literal or untyped
+ * NULL) is coerced to text before the format cast lookup, so that, e.g.,
+ * CAST('2026-06-24' AS date FORMAT 'YYYY-MM-DD') uses a format cast
+ * registered for (text, date) rather than requiring one for (unknown,
+ * date).
+ */
+ if (sourceType == UNKNOWNOID)
+ {
+ expr = coerce_to_specific_type(pstate, expr, TEXTOID, "CAST");
+ sourceType = exprType(expr);
+ }
+
+ location = tc->location;
+ if (location < 0)
+ location = tc->typeName->location;
+
+ /*
+ * Transform the FORMAT expression and coerce it to text before the
+ * format cast lookup, so an invalid FORMAT expression reports a normal
+ * error rather than being masked by a missing-format cast error.
+ */
+ fmt = transformExprRecurse(pstate, tc->format);
+ fmt = coerce_to_specific_type(pstate, fmt, TEXTOID, "FORMAT");
+
+ /*
+ * Look up the format cast for this (source, target) pair. A FORMAT clause
+ * never falls back to ordinary cast resolution.
+ */
+ formatcastid = get_format_cast_function(sourceType, targetType, &fmtfuncid);
+ if (!OidIsValid(formatcastid))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("format cast from type %s to type %s does not exist",
+ format_type_be(sourceType),
+ format_type_be(targetType)),
+ errhint("Use CREATE FORMAT CAST to define a format cast for this type pair."),
+ parser_errposition(pstate, location)));
+
+ /*
+ * Build the CoerceViaFormatCast node. Its result type is the format cast
+ * function's return type, which CREATE FORMAT CAST guarantees equals the
+ * target type.
+ *
+ * The collation fields are left unset here; they are assigned later by
+ * parse_collate.c, where CoerceViaFormatCast takes the same general n-ary
+ * expression path as FuncExpr (its result/input collation get/set hooks
+ * live in nodeFuncs.c).
+ */
+ funcrettype = get_func_rettype(fmtfuncid);
+ if (!OidIsValid(funcrettype))
+ elog(ERROR, "cache lookup failed for function %u", fmtfuncid);
+ Assert(funcrettype == targetType);
+
+ cvf = makeNode(CoerceViaFormatCast);
+ cvf->arg = (Expr *) expr;
+ cvf->format = (Expr *) fmt;
+ cvf->resulttype = funcrettype;
+ cvf->resulttypmod = -1; /* typmod enforced below, if any */
+ cvf->resultcollid = InvalidOid;
+ cvf->inputcollid = InvalidOid;
+ cvf->formatfunc = fmtfuncid;
+ cvf->formatcastid = formatcastid;
+ cvf->coercionformat = COERCE_EXPLICIT_CAST;
+ cvf->location = location;
+ result = (Node *) cvf;
+
+ /*
+ * Enforce the declared target type modifier (and any domain constraints)
+ * using the ordinary coercion machinery. In the common case where the
+ * target has no typmod and is not a domain this is a no-op; otherwise it
+ * layers the same length coercion / domain check that an ordinary cast to
+ * the same target would apply.
+ */
+ result = coerce_to_target_type(pstate, result, funcrettype,
+ targetType, targetTypmod,
+ COERCION_EXPLICIT,
+ COERCE_EXPLICIT_CAST,
+ location);
+ if (result == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_CANNOT_COERCE),
+ errmsg("cannot cast type %s to %s",
+ format_type_be(funcrettype),
+ format_type_be(targetType)),
+ parser_errposition(pstate, location)));
+
+ return result;
+}
+
/*
* Handle an explicit COLLATE clause.
*
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 88de5c0481c..eb0eaf37da8 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9447,6 +9447,10 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags)
/* function-like: name(..) or name[..] */
return true;
+ case T_CoerceViaFormatCast:
+ /* deparses as CAST(.. FORMAT ..), self-delimiting */
+ return true;
+
/* CASE keywords act as parentheses */
case T_CaseExpr:
return true;
@@ -10310,6 +10314,21 @@ get_rule_expr(Node *node, deparse_context *context,
}
break;
+ case T_CoerceViaFormatCast:
+ {
+ CoerceViaFormatCast *fmt = (CoerceViaFormatCast *) node;
+
+ /* always print the SQL-standard CAST(... FORMAT ...) syntax */
+ appendStringInfoString(buf, "CAST(");
+ get_rule_expr((Node *) fmt->arg, context, false);
+ appendStringInfo(buf, " AS %s FORMAT ",
+ format_type_with_typemod(fmt->resulttype,
+ fmt->resulttypmod));
+ get_rule_expr((Node *) fmt->format, context, false);
+ appendStringInfoChar(buf, ')');
+ }
+ break;
+
case T_ArrayCoerceExpr:
{
ArrayCoerceExpr *acoerce = (ArrayCoerceExpr *) node;
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 036de5f79ef..76b720668b5 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -27,6 +27,7 @@
#include "catalog/pg_collation.h"
#include "catalog/pg_constraint.h"
#include "catalog/pg_database.h"
+#include "catalog/pg_format_cast.h"
#include "catalog/pg_index.h"
#include "catalog/pg_language.h"
#include "catalog/pg_namespace.h"
@@ -1238,6 +1239,42 @@ get_cast_oid(Oid sourcetypeid, Oid targettypeid, bool missing_ok)
return oid;
}
+/* ---------- PG_FORMAT_CAST CACHE ---------- */
+
+/*
+ * get_format_cast_function
+ *
+ * Given source and target type OIDs, look up the format cast registered
+ * for that (source, target) pair. Returns the pg_format_cast row OID, or
+ * InvalidOid if none is registered. If found and formatfunc is not
+ * NULL, *formatfunc is set to the format cast function OID.
+ */
+Oid
+get_format_cast_function(Oid sourcetypeid, Oid targettypeid, Oid *formatfunc)
+{
+ HeapTuple tp;
+ Oid result;
+
+ tp = SearchSysCache2(FORMATCASTSOURCETARGET,
+ ObjectIdGetDatum(sourcetypeid),
+ ObjectIdGetDatum(targettypeid));
+ if (!HeapTupleIsValid(tp))
+ return InvalidOid;
+
+ {
+ Form_pg_format_cast fmt = (Form_pg_format_cast) GETSTRUCT(tp);
+
+ /* A valid pg_format_cast row always names a format cast function. */
+ Assert(OidIsValid(fmt->fmtfunc));
+
+ result = fmt->oid;
+ if (formatfunc != NULL)
+ *formatfunc = fmt->fmtfunc;
+ }
+ ReleaseSysCache(tp);
+ return result;
+}
+
/* ---------- COLLATION CACHE ---------- */
/*
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index bb05aeebee4..4aaebcb0307 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1230,6 +1230,44 @@ typedef struct CoerceViaIO
ParseLoc location; /* token location, or -1 if unknown */
} CoerceViaIO;
+/* ----------------
+ * CoerceViaFormatCast
+ *
+ * CoerceViaFormatCast represents CAST(arg AS resulttype FORMAT format), a
+ * formatted cast resolved through the pg_format_cast catalog. It calls the
+ * registered format cast function with the source value and the FORMAT
+ * expression (already coerced to text), returning the target type:
+ *
+ * formatfunc(arg, format) returns resulttype
+ *
+ * Unlike a plain FuncExpr, this node remembers that it came from
+ * CAST ... FORMAT (so it deparses back to that syntax) and which
+ * pg_format_cast row it resolved to (so a stored expression can depend on the
+ * format cast object). It is executed by reusing ordinary function-call
+ * evaluation, so EXECUTE permission on formatfunc is checked at run time,
+ * as for an ordinary function-backed cast.
+ * ----------------
+ */
+typedef struct CoerceViaFormatCast
+{
+ Expr xpr;
+ Expr *arg; /* source expression */
+ Expr *format; /* FORMAT expression, already coerced to text */
+ Oid resulttype; /* output type of the cast */
+ /* output typmod (usually -1; typmod enforcement is layered above) */
+ int32 resulttypmod pg_node_attr(query_jumble_ignore);
+ /* OID of result collation, or InvalidOid if none */
+ Oid resultcollid pg_node_attr(query_jumble_ignore);
+ /* input collation for the format cast function call */
+ Oid inputcollid pg_node_attr(query_jumble_ignore);
+ Oid formatfunc; /* pg_format_cast.fmtfunc */
+ /* pg_format_cast row OID this resolved to (for dependencies) */
+ Oid formatcastid pg_node_attr(query_jumble_ignore);
+ /* how to display this node */
+ CoercionForm coercionformat pg_node_attr(query_jumble_ignore);
+ ParseLoc location; /* token location, or -1 if unknown */
+} CoerceViaFormatCast;
+
/* ----------------
* ArrayCoerceExpr
*
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 865980cb0f1..53026b090a3 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -103,6 +103,8 @@ extern void get_atttypetypmodcoll(Oid relid, AttrNumber attnum,
Oid *typid, int32 *typmod, Oid *collid);
extern Datum get_attoptions(Oid relid, int16 attnum);
extern Oid get_cast_oid(Oid sourcetypeid, Oid targettypeid, bool missing_ok);
+extern Oid get_format_cast_function(Oid sourcetypeid, Oid targettypeid,
+ Oid *formatfunc);
extern char *get_collation_name(Oid colloid);
extern bool get_collation_isdeterministic(Oid colloid);
extern char *get_constraint_name(Oid conoid);
diff --git a/src/test/regress/expected/expressions.out b/src/test/regress/expected/expressions.out
index d39cd1b7fbd..1cbd05135dd 100644
--- a/src/test/regress/expected/expressions.out
+++ b/src/test/regress/expected/expressions.out
@@ -564,23 +564,27 @@ 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
+-- A FORMAT clause is resolved through a registered format cast (see CREATE
+-- FORMAT CAST) and never falls back to an ordinary cast. No format_casts are
+-- defined in this test, so these casts fail with a missing-format cast error;
+-- this confirms the FORMAT clause is neither ignored nor treated as an
+-- ordinary cast. An unknown-type source literal is coerced to text first,
+-- so the lookup key is (text, target).
+-- basic form (looks up a format cast for (text, date))
SELECT CAST('2026-06-24' AS date FORMAT 'YYYY-MM-DD');
-ERROR: formatted casts are not implemented yet
+ERROR: format cast from type text to type date does not exist
LINE 1: SELECT CAST('2026-06-24' AS date FORMAT 'YYYY-MM-DD');
- ^
-DETAIL: No format cast resolution mechanism is available.
+ ^
+HINT: Use CREATE FORMAT CAST to define a format cast for this type pair.
-- 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
+ERROR: format cast from type text to type date does not exist
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
+ ^
+HINT: Use CREATE FORMAT CAST to define a format cast for this type pair.
+-- a no-op-looking cast must not be relabeled away; it needs a (text, text) format cast
SELECT CAST('abc'::text AS text FORMAT 'whatever');
-ERROR: formatted casts are not implemented yet
+ERROR: format cast from type text to type text does not exist
LINE 1: SELECT CAST('abc'::text AS text FORMAT 'whatever');
- ^
-DETAIL: No format cast resolution mechanism is available.
+ ^
+HINT: Use CREATE FORMAT CAST to define a format cast for this type pair.
diff --git a/src/test/regress/expected/format_casts.out b/src/test/regress/expected/format_casts.out
index 2ae4ca0d0d0..cb8684080ef 100644
--- a/src/test/regress/expected/format_casts.out
+++ b/src/test/regress/expected/format_casts.out
@@ -1,9 +1,10 @@
--
-- FORMAT CASTS
--
--- CREATE/DROP FORMAT CAST registers format cast metadata in pg_format_cast,
--- keyed by a (source type, target type) pair. This is catalog and DDL
--- infrastructure only; it does not transform or execute formatted casts.
+-- A format cast registers a function for a (source type, target type) pair; a
+-- CAST(... AS target FORMAT format_expr) is resolved through pg_format_cast and
+-- calls that function. This test covers both the CREATE/DROP FORMAT CAST
+-- catalog DDL and the execution of formatted casts.
-- A simple format cast function with the required signature
-- function(source_type, text) returns target_type
CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text
@@ -104,14 +105,124 @@ CREATE FUNCTION fmt_anyel(anyelement, text) RETURNS text
CREATE FORMAT CAST (anyelement AS text)
WITH FUNCTION fmt_anyel(anyelement, text);
ERROR: source data type anyelement is a pseudo-type
--- Registering a format cast does not enable a formatted cast: the FORMAT
--- clause must not be silently ignored or rewritten to a built-in function,
--- so CAST(... FORMAT ...) is rejected during parse analysis.
-SELECT CAST(5 AS text FORMAT 'YYYY');
-ERROR: formatted casts are not implemented yet
-LINE 1: SELECT CAST(5 AS text FORMAT 'YYYY');
+-- ====================================================================
+-- Execution: a formatted cast resolves through pg_format_cast and calls the
+-- registered format cast function.
+-- ====================================================================
+-- basic execution, using the (integer, text) format cast created above
+SELECT CAST(5 AS text FORMAT 'abc');
+ text
+-------
+ 5:abc
+(1 row)
+
+-- the FORMAT expression may be any expression, coerced to text
+SELECT CAST(5 AS text FORMAT 'a' || 'b');
+ text
+------
+ 5:ab
+(1 row)
+
+SELECT CAST(5 AS text FORMAT 123);
+ text
+-------
+ 5:123
+(1 row)
+
+-- The FORMAT expression is parse-analyzed independently of (and before) the
+-- format cast lookup, so an invalid FORMAT expression reports a normal error.
+SELECT CAST(5 AS text FORMAT no_such_column);
+ERROR: column "no_such_column" does not exist
+LINE 1: SELECT CAST(5 AS text FORMAT no_such_column);
^
-DETAIL: No format cast resolution mechanism is available.
+-- Like an ordinary cast that uses a cast function, a formatted cast checks
+-- EXECUTE on the format cast function at use time.
+REVOKE EXECUTE ON FUNCTION int4_to_text_fmt(integer, text) FROM PUBLIC;
+CREATE ROLE regress_format_cast_noexec NOLOGIN;
+SET ROLE regress_format_cast_noexec;
+SELECT CAST(5 AS text FORMAT 'p'); -- fails: no EXECUTE on format cast function
+ERROR: permission denied for function int4_to_text_fmt
+RESET ROLE;
+DROP ROLE regress_format_cast_noexec;
+GRANT EXECUTE ON FUNCTION int4_to_text_fmt(integer, text) TO PUBLIC;
+-- A FORMAT clause never falls back to an ordinary cast: text -> text is a
+-- trivial ordinary cast, but a formatted cast still requires a format cast.
+SELECT CAST('abc'::text AS text FORMAT 'whatever');
+ERROR: format cast from type text to type text does not exist
+LINE 1: SELECT CAST('abc'::text AS text FORMAT 'whatever');
+ ^
+HINT: Use CREATE FORMAT CAST to define a format cast for this type pair.
+-- An unknown-type source literal is coerced to text first, so this also looks
+-- up (text, text), not (unknown, text).
+SELECT CAST('abc' AS text FORMAT 'fmt');
+ERROR: format cast from type text to type text does not exist
+LINE 1: SELECT CAST('abc' AS text FORMAT 'fmt');
+ ^
+HINT: Use CREATE FORMAT CAST to define a format cast for this type pair.
+-- A missing format cast is an error even where an ordinary cast would be valid.
+SELECT CAST(5 AS integer FORMAT 'x');
+ERROR: format cast from type integer to type integer does not exist
+LINE 1: SELECT CAST(5 AS integer FORMAT 'x');
+ ^
+HINT: Use CREATE FORMAT CAST to define a format cast for this type pair.
+-- Define the (text, text) format cast and re-run the text-source cases.
+CREATE FUNCTION text_to_text_fmt(text, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1 || '/' || $2;
+CREATE FORMAT CAST (text AS text)
+ WITH FUNCTION text_to_text_fmt(text, text);
+SELECT CAST('abc'::text AS text FORMAT 'whatever');
+ text
+--------------
+ abc/whatever
+(1 row)
+
+SELECT CAST('abc' AS text FORMAT 'fmt');
+ text
+---------
+ abc/fmt
+(1 row)
+
+-- A type modifier on the target is enforced through the ordinary coercion path.
+CREATE FUNCTION int4_to_vc_fmt(integer, text) RETURNS varchar
+ LANGUAGE sql IMMUTABLE RETURN $1::text || $2;
+CREATE FORMAT CAST (integer AS varchar)
+ WITH FUNCTION int4_to_vc_fmt(integer, text);
+SELECT CAST(5 AS varchar FORMAT 'XXXX');
+ varchar
+---------
+ 5XXXX
+(1 row)
+
+SELECT CAST(5 AS varchar(3) FORMAT 'XXXX'); -- length 3 enforced
+ varchar
+---------
+ 5XX
+(1 row)
+
+-- Domain target: the format cast must return the domain type, so the domain's
+-- constraints are enforced by the function's result.
+CREATE DOMAIN nonempty_text AS text CHECK (VALUE <> '');
+CREATE FUNCTION text_to_netext_fmt(text, text) RETURNS nonempty_text
+ LANGUAGE sql IMMUTABLE RETURN $2::nonempty_text;
+CREATE FORMAT CAST (text AS nonempty_text)
+ WITH FUNCTION text_to_netext_fmt(text, text);
+SELECT CAST('z'::text AS nonempty_text FORMAT 'ok');
+ nonempty_text
+---------------
+ ok
+(1 row)
+
+SELECT CAST('z'::text AS nonempty_text FORMAT ''); -- domain check violation
+ERROR: value for domain nonempty_text violates check constraint "nonempty_text_check"
+CONTEXT: SQL function "text_to_netext_fmt" statement 1
+-- Drop the objects created in this execution section.
+DROP FORMAT CAST (text AS text);
+DROP FUNCTION text_to_text_fmt(text, text);
+DROP FORMAT CAST (integer AS varchar);
+DROP FUNCTION int4_to_vc_fmt(integer, text);
+DROP FORMAT CAST (text AS nonempty_text);
+DROP FUNCTION text_to_netext_fmt(text, text);
+DROP DOMAIN nonempty_text;
-- Dependency behavior: the format cast depends on its function.
DROP FUNCTION int4_to_text_fmt(integer, text); -- fails (RESTRICT)
ERROR: cannot drop function int4_to_text_fmt(integer,text) because other objects depend on it
@@ -154,3 +265,135 @@ DROP FUNCTION fmt_bad_arg1(bigint, text);
DROP FUNCTION fmt_bad_ret(integer, text);
DROP FUNCTION fmt_bad_set(integer, text);
DROP FUNCTION fmt_anyel(anyelement, text);
+-- ====================================================================
+-- CoerceViaFormatCast: deparse fidelity and dependency on the format cast row
+-- ====================================================================
+CREATE FUNCTION i2t_view(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2;
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION i2t_view(integer, text);
+-- A view over a formatted cast deparses back to CAST(... FORMAT ...).
+CREATE VIEW format_cast_view AS SELECT CAST(5 AS text FORMAT 'abc') AS x;
+SELECT pg_get_viewdef('format_cast_view'::regclass, true);
+ pg_get_viewdef
+--------------------------------------------------
+ SELECT CAST(5 AS text FORMAT 'abc'::text) AS x;
+(1 row)
+
+SELECT * FROM format_cast_view;
+ x
+-------
+ 5:abc
+(1 row)
+
+-- The view depends on the pg_format_cast row, so DROP FORMAT CAST RESTRICT fails.
+DROP FORMAT CAST (integer AS text); -- fails (RESTRICT, view depends)
+ERROR: cannot drop format cast from integer to text because other objects depend on it
+DETAIL: view format_cast_view depends on format cast from integer to text
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+-- ... and CASCADE drops the dependent view.
+DROP FORMAT CAST (integer AS text) CASCADE;
+NOTICE: drop cascades to view format_cast_view
+SELECT count(*) FROM pg_class WHERE relname = 'format_cast_view';
+ count
+-------
+ 0
+(1 row)
+
+DROP FUNCTION i2t_view(integer, text);
+-- ====================================================================
+-- Collation: a formatted cast behaves like calling the format cast function
+-- formatfunc(arg, format). Its result collation is the result type's
+-- collation, and the input collation is derived from the arg and FORMAT
+-- expressions exactly as for an ordinary two-argument function call.
+-- ====================================================================
+-- text_larger(text, text) returns text and is collation-sensitive at the C
+-- level (it reads PG_GET_COLLATION), so it exercises the input collation.
+CREATE FORMAT CAST (text AS text)
+ WITH FUNCTION pg_catalog.text_larger(text, text);
+-- The default collation flows into the format cast call; if the input collation
+-- were left unset this would fail with "could not determine which collation".
+SELECT CAST('apple'::text AS text FORMAT 'banana'::text);
+ text
+--------
+ banana
+(1 row)
+
+-- An explicit COLLATE on an operand is honored as the input collation.
+SELECT CAST('apple'::text AS text FORMAT 'banana'::text COLLATE "C");
+ text
+--------
+ banana
+(1 row)
+
+-- The result is collatable, so an explicit COLLATE on the cast itself works.
+SELECT CAST('apple'::text AS text FORMAT 'banana'::text) COLLATE "C" < 'zzz';
+ ?column?
+----------
+ t
+(1 row)
+
+-- Conflicting explicit input collations are rejected, just like a plain
+-- function call text_larger('a' COLLATE "C", 'b' COLLATE "POSIX").
+SELECT CAST('a'::text COLLATE "C" AS text FORMAT 'b'::text COLLATE "POSIX");
+ERROR: collation mismatch between explicit collations "C" and "POSIX"
+LINE 1: ...ST('a'::text COLLATE "C" AS text FORMAT 'b'::text COLLATE "P...
+ ^
+DROP FORMAT CAST (text AS text);
+-- ====================================================================
+-- ruleutils: arg and FORMAT subexpressions deparse unambiguously inside
+-- CAST(...), needing no extra parentheses, and re-parse to the same thing.
+-- ====================================================================
+CREATE FUNCTION i2t_paren(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || $2;
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION i2t_paren(integer, text);
+CREATE VIEW format_cast_paren_view AS
+ SELECT CAST((1 + 2) AS text FORMAT ('a' || 'b')) AS x;
+SELECT pg_get_viewdef('format_cast_paren_view'::regclass, true);
+ pg_get_viewdef
+-----------------------------------------------------------------
+ SELECT CAST(1 + 2 AS text FORMAT 'a'::text || 'b'::text) AS x;
+(1 row)
+
+SELECT * FROM format_cast_paren_view;
+ x
+-----
+ 3ab
+(1 row)
+
+DROP VIEW format_cast_paren_view;
+DROP FORMAT CAST (integer AS text);
+DROP FUNCTION i2t_paren(integer, text);
+-- ====================================================================
+-- Dependency completeness: a view over a formatted cast depends (through the
+-- pg_format_cast row) on the format cast function, so dropping the function with
+-- CASCADE removes both the format cast row and the view in one step.
+-- ====================================================================
+CREATE FUNCTION i2t_chain(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || $2;
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION i2t_chain(integer, text);
+CREATE VIEW format_cast_chain_view AS SELECT CAST(7 AS text FORMAT 'q') AS x;
+DROP FUNCTION i2t_chain(integer, text); -- fails: format cast + view depend
+ERROR: cannot drop function i2t_chain(integer,text) because other objects depend on it
+DETAIL: format cast from integer to text depends on function i2t_chain(integer,text)
+view format_cast_chain_view depends on function i2t_chain(integer,text)
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+DROP FUNCTION i2t_chain(integer, text) CASCADE; -- drops format cast and view
+NOTICE: drop cascades to 2 other objects
+DETAIL: drop cascades to format cast from integer to text
+drop cascades to view format_cast_chain_view
+SELECT count(*) FROM pg_format_cast
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype;
+ count
+-------
+ 0
+(1 row)
+
+SELECT count(*) FROM pg_class WHERE relname = 'format_cast_chain_view';
+ count
+-------
+ 0
+(1 row)
+
diff --git a/src/test/regress/sql/expressions.sql b/src/test/regress/sql/expressions.sql
index 36eae8c3f04..df4f5b20466 100644
--- a/src/test/regress/sql/expressions.sql
+++ b/src/test/regress/sql/expressions.sql
@@ -305,14 +305,18 @@ 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
+-- A FORMAT clause is resolved through a registered format cast (see CREATE
+-- FORMAT CAST) and never falls back to an ordinary cast. No format_casts are
+-- defined in this test, so these casts fail with a missing-format cast error;
+-- this confirms the FORMAT clause is neither ignored nor treated as an
+-- ordinary cast. An unknown-type source literal is coerced to text first,
+-- so the lookup key is (text, target).
+
+-- basic form (looks up a format cast for (text, date))
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
+-- a no-op-looking cast must not be relabeled away; it needs a (text, text) format cast
SELECT CAST('abc'::text AS text FORMAT 'whatever');
diff --git a/src/test/regress/sql/format_casts.sql b/src/test/regress/sql/format_casts.sql
index 4453a09784e..05d3236b5ac 100644
--- a/src/test/regress/sql/format_casts.sql
+++ b/src/test/regress/sql/format_casts.sql
@@ -1,9 +1,10 @@
--
-- FORMAT CASTS
--
--- CREATE/DROP FORMAT CAST registers format cast metadata in pg_format_cast,
--- keyed by a (source type, target type) pair. This is catalog and DDL
--- infrastructure only; it does not transform or execute formatted casts.
+-- A format cast registers a function for a (source type, target type) pair; a
+-- CAST(... AS target FORMAT format_expr) is resolved through pg_format_cast and
+-- calls that function. This test covers both the CREATE/DROP FORMAT CAST
+-- catalog DDL and the execution of formatted casts.
-- A simple format cast function with the required signature
-- function(source_type, text) returns target_type
@@ -86,10 +87,74 @@ CREATE FUNCTION fmt_anyel(anyelement, text) RETURNS text
CREATE FORMAT CAST (anyelement AS text)
WITH FUNCTION fmt_anyel(anyelement, text);
--- Registering a format cast does not enable a formatted cast: the FORMAT
--- clause must not be silently ignored or rewritten to a built-in function,
--- so CAST(... FORMAT ...) is rejected during parse analysis.
-SELECT CAST(5 AS text FORMAT 'YYYY');
+-- ====================================================================
+-- Execution: a formatted cast resolves through pg_format_cast and calls the
+-- registered format cast function.
+-- ====================================================================
+
+-- basic execution, using the (integer, text) format cast created above
+SELECT CAST(5 AS text FORMAT 'abc');
+-- the FORMAT expression may be any expression, coerced to text
+SELECT CAST(5 AS text FORMAT 'a' || 'b');
+SELECT CAST(5 AS text FORMAT 123);
+
+-- The FORMAT expression is parse-analyzed independently of (and before) the
+-- format cast lookup, so an invalid FORMAT expression reports a normal error.
+SELECT CAST(5 AS text FORMAT no_such_column);
+
+-- Like an ordinary cast that uses a cast function, a formatted cast checks
+-- EXECUTE on the format cast function at use time.
+REVOKE EXECUTE ON FUNCTION int4_to_text_fmt(integer, text) FROM PUBLIC;
+CREATE ROLE regress_format_cast_noexec NOLOGIN;
+SET ROLE regress_format_cast_noexec;
+SELECT CAST(5 AS text FORMAT 'p'); -- fails: no EXECUTE on format cast function
+RESET ROLE;
+DROP ROLE regress_format_cast_noexec;
+GRANT EXECUTE ON FUNCTION int4_to_text_fmt(integer, text) TO PUBLIC;
+
+-- A FORMAT clause never falls back to an ordinary cast: text -> text is a
+-- trivial ordinary cast, but a formatted cast still requires a format cast.
+SELECT CAST('abc'::text AS text FORMAT 'whatever');
+-- An unknown-type source literal is coerced to text first, so this also looks
+-- up (text, text), not (unknown, text).
+SELECT CAST('abc' AS text FORMAT 'fmt');
+-- A missing format cast is an error even where an ordinary cast would be valid.
+SELECT CAST(5 AS integer FORMAT 'x');
+
+-- Define the (text, text) format cast and re-run the text-source cases.
+CREATE FUNCTION text_to_text_fmt(text, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1 || '/' || $2;
+CREATE FORMAT CAST (text AS text)
+ WITH FUNCTION text_to_text_fmt(text, text);
+SELECT CAST('abc'::text AS text FORMAT 'whatever');
+SELECT CAST('abc' AS text FORMAT 'fmt');
+
+-- A type modifier on the target is enforced through the ordinary coercion path.
+CREATE FUNCTION int4_to_vc_fmt(integer, text) RETURNS varchar
+ LANGUAGE sql IMMUTABLE RETURN $1::text || $2;
+CREATE FORMAT CAST (integer AS varchar)
+ WITH FUNCTION int4_to_vc_fmt(integer, text);
+SELECT CAST(5 AS varchar FORMAT 'XXXX');
+SELECT CAST(5 AS varchar(3) FORMAT 'XXXX'); -- length 3 enforced
+
+-- Domain target: the format cast must return the domain type, so the domain's
+-- constraints are enforced by the function's result.
+CREATE DOMAIN nonempty_text AS text CHECK (VALUE <> '');
+CREATE FUNCTION text_to_netext_fmt(text, text) RETURNS nonempty_text
+ LANGUAGE sql IMMUTABLE RETURN $2::nonempty_text;
+CREATE FORMAT CAST (text AS nonempty_text)
+ WITH FUNCTION text_to_netext_fmt(text, text);
+SELECT CAST('z'::text AS nonempty_text FORMAT 'ok');
+SELECT CAST('z'::text AS nonempty_text FORMAT ''); -- domain check violation
+
+-- Drop the objects created in this execution section.
+DROP FORMAT CAST (text AS text);
+DROP FUNCTION text_to_text_fmt(text, text);
+DROP FORMAT CAST (integer AS varchar);
+DROP FUNCTION int4_to_vc_fmt(integer, text);
+DROP FORMAT CAST (text AS nonempty_text);
+DROP FUNCTION text_to_netext_fmt(text, text);
+DROP DOMAIN nonempty_text;
-- Dependency behavior: the format cast depends on its function.
DROP FUNCTION int4_to_text_fmt(integer, text); -- fails (RESTRICT)
@@ -124,3 +189,77 @@ DROP FUNCTION fmt_bad_arg1(bigint, text);
DROP FUNCTION fmt_bad_ret(integer, text);
DROP FUNCTION fmt_bad_set(integer, text);
DROP FUNCTION fmt_anyel(anyelement, text);
+
+-- ====================================================================
+-- CoerceViaFormatCast: deparse fidelity and dependency on the format cast row
+-- ====================================================================
+CREATE FUNCTION i2t_view(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2;
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION i2t_view(integer, text);
+
+-- A view over a formatted cast deparses back to CAST(... FORMAT ...).
+CREATE VIEW format_cast_view AS SELECT CAST(5 AS text FORMAT 'abc') AS x;
+SELECT pg_get_viewdef('format_cast_view'::regclass, true);
+SELECT * FROM format_cast_view;
+
+-- The view depends on the pg_format_cast row, so DROP FORMAT CAST RESTRICT fails.
+DROP FORMAT CAST (integer AS text); -- fails (RESTRICT, view depends)
+-- ... and CASCADE drops the dependent view.
+DROP FORMAT CAST (integer AS text) CASCADE;
+SELECT count(*) FROM pg_class WHERE relname = 'format_cast_view';
+DROP FUNCTION i2t_view(integer, text);
+
+-- ====================================================================
+-- Collation: a formatted cast behaves like calling the format cast function
+-- formatfunc(arg, format). Its result collation is the result type's
+-- collation, and the input collation is derived from the arg and FORMAT
+-- expressions exactly as for an ordinary two-argument function call.
+-- ====================================================================
+-- text_larger(text, text) returns text and is collation-sensitive at the C
+-- level (it reads PG_GET_COLLATION), so it exercises the input collation.
+CREATE FORMAT CAST (text AS text)
+ WITH FUNCTION pg_catalog.text_larger(text, text);
+-- The default collation flows into the format cast call; if the input collation
+-- were left unset this would fail with "could not determine which collation".
+SELECT CAST('apple'::text AS text FORMAT 'banana'::text);
+-- An explicit COLLATE on an operand is honored as the input collation.
+SELECT CAST('apple'::text AS text FORMAT 'banana'::text COLLATE "C");
+-- The result is collatable, so an explicit COLLATE on the cast itself works.
+SELECT CAST('apple'::text AS text FORMAT 'banana'::text) COLLATE "C" < 'zzz';
+-- Conflicting explicit input collations are rejected, just like a plain
+-- function call text_larger('a' COLLATE "C", 'b' COLLATE "POSIX").
+SELECT CAST('a'::text COLLATE "C" AS text FORMAT 'b'::text COLLATE "POSIX");
+DROP FORMAT CAST (text AS text);
+
+-- ====================================================================
+-- ruleutils: arg and FORMAT subexpressions deparse unambiguously inside
+-- CAST(...), needing no extra parentheses, and re-parse to the same thing.
+-- ====================================================================
+CREATE FUNCTION i2t_paren(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || $2;
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION i2t_paren(integer, text);
+CREATE VIEW format_cast_paren_view AS
+ SELECT CAST((1 + 2) AS text FORMAT ('a' || 'b')) AS x;
+SELECT pg_get_viewdef('format_cast_paren_view'::regclass, true);
+SELECT * FROM format_cast_paren_view;
+DROP VIEW format_cast_paren_view;
+DROP FORMAT CAST (integer AS text);
+DROP FUNCTION i2t_paren(integer, text);
+
+-- ====================================================================
+-- Dependency completeness: a view over a formatted cast depends (through the
+-- pg_format_cast row) on the format cast function, so dropping the function with
+-- CASCADE removes both the format cast row and the view in one step.
+-- ====================================================================
+CREATE FUNCTION i2t_chain(integer, text) RETURNS text
+ LANGUAGE sql IMMUTABLE RETURN $1::text || $2;
+CREATE FORMAT CAST (integer AS text)
+ WITH FUNCTION i2t_chain(integer, text);
+CREATE VIEW format_cast_chain_view AS SELECT CAST(7 AS text FORMAT 'q') AS x;
+DROP FUNCTION i2t_chain(integer, text); -- fails: format cast + view depend
+DROP FUNCTION i2t_chain(integer, text) CASCADE; -- drops format cast and view
+SELECT count(*) FROM pg_format_cast
+ WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype;
+SELECT count(*) FROM pg_class WHERE relname = 'format_cast_chain_view';
--
2.54.0
[application/octet-stream] v2-0001-Add-parser-support-for-CAST-FORMAT.patch (5.6K, ../../CABXr29HF+AHV0FNxQfHyN-ByW6-3+pTBe5Pxm23wRpBYOmfohA@mail.gmail.com/5-v2-0001-Add-parser-support-for-CAST-FORMAT.patch)
download | inline diff:
From 82658dcbbd4a45844ae670e27cf57faf1ab1db7f Mon Sep 17 00:00:00 2001
From: Haibo Yan <[email protected]>
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
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-01 13:44 jian he <[email protected]>
parent: Haibo Yan <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: jian he @ 2026-07-01 13:44 UTC (permalink / raw)
To: Haibo Yan <[email protected]>; +Cc: Robert Haas <[email protected]>; Corey Huinker <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; pgsql-hackers
On Wed, Jul 1, 2026 at 1:39 AM Haibo Yan <[email protected]> wrote:
>
> 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.
>
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:
CAST('{2022-01-01, 2022-21-01}' AS DATE[] FORMAT 'YYYY-DD-MM');
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-...
[3]: https://www.postgresql.org/message-id/CACJufxGVuCM4XFGqaqiV-VOEiqMtCZ3%2BT-%2BSrG-y6kqdLo1ZqA%40mail...
--
jian
https://www.enterprisedb.com/
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-01 16:45 Haibo Yan <[email protected]>
parent: jian he <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: Haibo Yan @ 2026-07-01 16:45 UTC (permalink / raw)
To: jian he <[email protected]>; +Cc: Robert Haas <[email protected]>; Corey Huinker <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; pgsql-hackers
Hi Jian,
Thanks for looking at this.
On Wed, Jul 1, 2026 at 6:45 AM jian he <[email protected]> wrote:
>
> On Wed, Jul 1, 2026 at 1:39 AM Haibo Yan <[email protected]> wrote:
> >
> > 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.
> >
>
> 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?
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.
>
> 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:
>
> CAST('{2022-01-01, 2022-21-01}' AS DATE[] FORMAT 'YYYY-DD-MM');
>
> 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).
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.
>
> 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.
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.
>
> [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-...
> [3]: https://www.postgresql.org/message-id/CACJufxGVuCM4XFGqaqiV-VOEiqMtCZ3%2BT-%2BSrG-y6kqdLo1ZqA%40mail...
>
>
>
> --
> jian
> https://www.enterprisedb.com/
Regards,
Haibo
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-01 20:58 David G. Johnston <[email protected]>
parent: Haibo Yan <[email protected]>
0 siblings, 2 replies; 53+ messages in thread
From: David G. Johnston @ 2026-07-01 20:58 UTC (permalink / raw)
To: Haibo Yan <[email protected]>; +Cc: jian he <[email protected]>; Robert Haas <[email protected]>; Corey Huinker <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; pgsql-hackers
On Wed, Jul 1, 2026 at 9:45 AM Haibo Yan <[email protected]> wrote:
> Hi Jian,
>
> Thanks for looking at this.
>
> On Wed, Jul 1, 2026 at 6:45 AM jian he <[email protected]>
> wrote:
> >
> > On Wed, Jul 1, 2026 at 1:39 AM Haibo Yan <[email protected]> wrote:
> > >
> > > 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.
> > >
> >
> > 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?
>
> 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.
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.
> >
> > 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:
> >
> > CAST('{2022-01-01, 2022-21-01}' AS DATE[] FORMAT 'YYYY-DD-MM');
> >
> > 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?
>
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.
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-02 00:00 Haibo Yan <[email protected]>
parent: David G. Johnston <[email protected]>
1 sibling, 1 reply; 53+ messages in thread
From: Haibo Yan @ 2026-07-02 00:00 UTC (permalink / raw)
To: David G. Johnston <[email protected]>; +Cc: jian he <[email protected]>; Robert Haas <[email protected]>; Corey Huinker <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; pgsql-hackers
Hi David,
Thanks, this is helpful.
On Wed, Jul 1, 2026 at 1:59 PM David G. Johnston
<[email protected]> wrote:
>
> On Wed, Jul 1, 2026 at 9:45 AM Haibo Yan <[email protected]> wrote:
>>
>> Hi Jian,
>>
>> Thanks for looking at this.
>>
>> On Wed, Jul 1, 2026 at 6:45 AM jian he <[email protected]> wrote:
>> >
>> > On Wed, Jul 1, 2026 at 1:39 AM Haibo Yan <[email protected]> wrote:
>> > >
>> > > 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.
>> > >
>> >
>> > 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?
>>
>> 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.
>
>
> 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
CAST(expr AS type FORMAT fmt)
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.
>>
>> >
>> > 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:
>> >
>> > CAST('{2022-01-01, 2022-21-01}' AS DATE[] FORMAT 'YYYY-DD-MM');
>> >
>> > 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?
>
>
> 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"
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
function(source_element_type, text) returns target_element_type
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
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-02 03:23 jian he <[email protected]>
parent: Haibo Yan <[email protected]>
0 siblings, 2 replies; 53+ messages in thread
From: jian he @ 2026-07-02 03:23 UTC (permalink / raw)
To: Haibo Yan <[email protected]>; +Cc: David G. Johnston <[email protected]>; Robert Haas <[email protected]>; Corey Huinker <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; pgsql-hackers
On Thu, Jul 2, 2026 at 8:01 AM Haibo Yan <[email protected]> wrote:
>
> 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
>
> function(source_element_type, text) returns target_element_type
>
> 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.
>
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...
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-02 04:08 David G. Johnston <[email protected]>
parent: jian he <[email protected]>
1 sibling, 0 replies; 53+ messages in thread
From: David G. Johnston @ 2026-07-02 04:08 UTC (permalink / raw)
To: jian he <[email protected]>; +Cc: Haibo Yan <[email protected]>; Robert Haas <[email protected]>; Corey Huinker <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; pgsql-hackers
On Wednesday, July 1, 2026, jian he <[email protected]> wrote:
>
> I guess not supporting arrays in the initial patch should be fine,
> 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.
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-02 04:14 Haibo Yan <[email protected]>
parent: jian he <[email protected]>
1 sibling, 0 replies; 53+ messages in thread
From: Haibo Yan @ 2026-07-02 04:14 UTC (permalink / raw)
To: jian he <[email protected]>; +Cc: David G. Johnston <[email protected]>; Robert Haas <[email protected]>; Corey Huinker <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; pgsql-hackers
On Wed, Jul 1, 2026 at 8:23 PM jian he <[email protected]> wrote:
>
> On Thu, Jul 2, 2026 at 8:01 AM Haibo Yan <[email protected]> wrote:
> >
> > 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
> >
> > function(source_element_type, text) returns target_element_type
> >
> > 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.
> >
>
> 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.
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.
>
> I guess not supporting arrays in the initial patch should be fine,
> 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.
> That raises another question: for user-defined CREATE FORMAT CAST,
> 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.
>
> [1]: https://www.postgresql.org/message-id/CACJufxGVuCM4XFGqaqiV-VOEiqMtCZ3%2BT-%2BSrG-y6kqdLo1ZqA%40mail...
Regards,
Haibo
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-02 15:13 Peter Eisentraut <[email protected]>
parent: Robert Haas <[email protected]>
2 siblings, 3 replies; 53+ messages in thread
From: Peter Eisentraut @ 2026-07-02 15:13 UTC (permalink / raw)
To: Robert Haas <[email protected]>; Corey Huinker <[email protected]>; +Cc: jian he <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; pgsql-hackers
On 18.06.26 16:51, Robert Haas wrote:
> 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.
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?
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-02 20:24 Robert Haas <[email protected]>
parent: Peter Eisentraut <[email protected]>
2 siblings, 0 replies; 53+ messages in thread
From: Robert Haas @ 2026-07-02 20:24 UTC (permalink / raw)
To: Peter Eisentraut <[email protected]>; +Cc: Corey Huinker <[email protected]>; jian he <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; pgsql-hackers
On Thu, Jul 2, 2026 at 11:13 AM Peter Eisentraut <[email protected]> wrote
> 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?
Standards compliance, maybe? I don't know what else there could be.
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-02 20:37 Robert Haas <[email protected]>
parent: David G. Johnston <[email protected]>
1 sibling, 1 reply; 53+ messages in thread
From: Robert Haas @ 2026-07-02 20:37 UTC (permalink / raw)
To: David G. Johnston <[email protected]>; +Cc: Haibo Yan <[email protected]>; jian he <[email protected]>; Corey Huinker <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; pgsql-hackers
On Wed, Jul 1, 2026 at 4:59 PM David G. Johnston
<[email protected]> 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.
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.
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-02 20:51 David G. Johnston <[email protected]>
parent: Robert Haas <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: David G. Johnston @ 2026-07-02 20:51 UTC (permalink / raw)
To: Robert Haas <[email protected]>; +Cc: Haibo Yan <[email protected]>; jian he <[email protected]>; Corey Huinker <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; pgsql-hackers
On Thursday, July 2, 2026, Robert Haas <[email protected]> wrote:
> On Wed, Jul 1, 2026 at 4:59 PM David G. Johnston
> <[email protected]> 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.
>
> I don't see how this would work. The existence of a regular cast
> 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
>
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.
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.
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-02 21:16 Robert Haas <[email protected]>
parent: David G. Johnston <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: Robert Haas @ 2026-07-02 21:16 UTC (permalink / raw)
To: David G. Johnston <[email protected]>; +Cc: Haibo Yan <[email protected]>; jian he <[email protected]>; Corey Huinker <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; pgsql-hackers
On Thu, Jul 2, 2026 at 4:51 PM David G. Johnston
<[email protected]> wrote:
>> and the existence of a
>> format cast doesn't mean there has to be a regular cast.
>
> Disagree
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.
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-02 21:52 David G. Johnston <[email protected]>
parent: Robert Haas <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: David G. Johnston @ 2026-07-02 21:52 UTC (permalink / raw)
To: Robert Haas <[email protected]>; +Cc: Haibo Yan <[email protected]>; jian he <[email protected]>; Corey Huinker <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; pgsql-hackers
On Thursday, July 2, 2026, Robert Haas <[email protected]> wrote:
> On Thu, Jul 2, 2026 at 4:51 PM David G. Johnston
> <[email protected]> wrote:
> >> and the existence of a
> >> format cast doesn't mean there has to be a regular cast.
> >
> > Disagree
>
> 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
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.
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-02 22:23 Robert Haas <[email protected]>
parent: David G. Johnston <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: Robert Haas @ 2026-07-02 22:23 UTC (permalink / raw)
To: David G. Johnston <[email protected]>; +Cc: Haibo Yan <[email protected]>; jian he <[email protected]>; Corey Huinker <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; pgsql-hackers
On Thu, Jul 2, 2026 at 5:52 PM David G. Johnston
<[email protected]> 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.
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-03 04:08 David G. Johnston <[email protected]>
parent: Robert Haas <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: David G. Johnston @ 2026-07-03 04:08 UTC (permalink / raw)
To: Robert Haas <[email protected]>; +Cc: Haibo Yan <[email protected]>; jian he <[email protected]>; Corey Huinker <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; pgsql-hackers
On Thu, Jul 2, 2026 at 3:23 PM Robert Haas <[email protected]> wrote:
> On Thu, Jul 2, 2026 at 5:52 PM David G. Johnston
> <[email protected]> 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.
>
> I don't really know how to have a productive conversation about this
> 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.
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-03 09:49 Robert Haas <[email protected]>
parent: David G. Johnston <[email protected]>
0 siblings, 0 replies; 53+ messages in thread
From: Robert Haas @ 2026-07-03 09:49 UTC (permalink / raw)
To: David G. Johnston <[email protected]>; +Cc: Haibo Yan <[email protected]>; jian he <[email protected]>; Corey Huinker <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; pgsql-hackers
On Fri, Jul 3, 2026 at 12:09 AM David G. Johnston
<[email protected]> 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...).
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-04 01:09 jian he <[email protected]>
parent: Peter Eisentraut <[email protected]>
2 siblings, 1 reply; 53+ messages in thread
From: jian he @ 2026-07-04 01:09 UTC (permalink / raw)
To: Peter Eisentraut <[email protected]>; +Cc: Robert Haas <[email protected]>; Corey Huinker <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; pgsql-hackers
On Thu, Jul 2, 2026 at 11:13 PM Peter Eisentraut <[email protected]> wrote:
>
> On 18.06.26 16:51, Robert Haas wrote:
> > 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.
>
> 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?
>
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>
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-06 17:14 Haibo Yan <[email protected]>
parent: Peter Eisentraut <[email protected]>
2 siblings, 2 replies; 53+ messages in thread
From: Haibo Yan @ 2026-07-06 17:14 UTC (permalink / raw)
To: Peter Eisentraut <[email protected]>; +Cc: Robert Haas <[email protected]>; Corey Huinker <[email protected]>; jian he <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; pgsql-hackers
On Thu, Jul 2, 2026 at 8:14 AM Peter Eisentraut <[email protected]> wrote:
>
> On 18.06.26 16:51, Robert Haas wrote:
> > 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.
>
> 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?
>
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
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-06 18:18 Robert Haas <[email protected]>
parent: Haibo Yan <[email protected]>
1 sibling, 1 reply; 53+ messages in thread
From: Robert Haas @ 2026-07-06 18:18 UTC (permalink / raw)
To: Haibo Yan <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Corey Huinker <[email protected]>; jian he <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; pgsql-hackers
On Mon, Jul 6, 2026 at 1:14 PM Haibo Yan <[email protected]> wrote:
> 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.
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.)
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-06 20:15 Haibo Yan <[email protected]>
parent: Robert Haas <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: Haibo Yan @ 2026-07-06 20:15 UTC (permalink / raw)
To: Robert Haas <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Corey Huinker <[email protected]>; jian he <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; pgsql-hackers
On Mon, Jul 6, 2026 at 11:18 AM Robert Haas <[email protected]> wrote:
>
> On Mon, Jul 6, 2026 at 1:14 PM Haibo Yan <[email protected]> wrote:
> > 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.
>
> 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.)
>
> --
> Robert Haas
> EDB: http://www.enterprisedb.com
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
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-06 20:27 Robert Haas <[email protected]>
parent: Haibo Yan <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: Robert Haas @ 2026-07-06 20:27 UTC (permalink / raw)
To: Haibo Yan <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Corey Huinker <[email protected]>; jian he <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; pgsql-hackers
On Mon, Jul 6, 2026 at 4:15 PM Haibo Yan <[email protected]> wrote:
> 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.
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.
> 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.
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...
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-07 03:56 Haibo Yan <[email protected]>
parent: Robert Haas <[email protected]>
0 siblings, 1 reply; 53+ messages in thread
From: Haibo Yan @ 2026-07-07 03:56 UTC (permalink / raw)
To: Robert Haas <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Corey Huinker <[email protected]>; jian he <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; pgsql-hackers
On Mon, Jul 6, 2026 at 1:27 PM Robert Haas <[email protected]> wrote:
>
> On Mon, Jul 6, 2026 at 4:15 PM Haibo Yan <[email protected]> wrote:
> > 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.
>
> 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.
>
> > 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.
>
> 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...
>
> --
> Robert Haas
> EDB: http://www.enterprisedb.com
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
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-07 12:07 Robert Haas <[email protected]>
parent: Haibo Yan <[email protected]>
0 siblings, 0 replies; 53+ messages in thread
From: Robert Haas @ 2026-07-07 12:07 UTC (permalink / raw)
To: Haibo Yan <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Corey Huinker <[email protected]>; jian he <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; pgsql-hackers
On Mon, Jul 6, 2026 at 11:56 PM Haibo Yan <[email protected]> wrote:
> 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.
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.
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-09 07:18 Peter Eisentraut <[email protected]>
parent: jian he <[email protected]>
0 siblings, 0 replies; 53+ messages in thread
From: Peter Eisentraut @ 2026-07-09 07:18 UTC (permalink / raw)
To: jian he <[email protected]>; +Cc: Robert Haas <[email protected]>; Corey Huinker <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; pgsql-hackers
On 04.07.26 03:09, jian he wrote:
>> 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?
>>
>
> 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>
The ON CONVERSION ERROR feature is independent of the FORMAT feature,
isn't it?
^ permalink raw reply [nested|flat] 53+ messages in thread
* Re: implement CAST(expr AS type FORMAT 'template')
@ 2026-07-09 07:20 Peter Eisentraut <[email protected]>
parent: Haibo Yan <[email protected]>
1 sibling, 0 replies; 53+ messages in thread
From: Peter Eisentraut @ 2026-07-09 07:20 UTC (permalink / raw)
To: Haibo Yan <[email protected]>; +Cc: Robert Haas <[email protected]>; Corey Huinker <[email protected]>; jian he <[email protected]>; Zsolt Parragi <[email protected]>; Vik Fearing <[email protected]>; David G. Johnston <[email protected]>; pgsql-hackers
On 06.07.26 19:14, Haibo Yan wrote:
> 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
One problem I see is that this would dictate that there is only one
possible format language for each of these conversions.
^ permalink raw reply [nested|flat] 53+ messages in thread
end of thread, other threads:[~2026-07-09 07:20 UTC | newest]
Thread overview: 53+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2025-07-27 15:43 implement CAST(expr AS type FORMAT 'template') jian he <[email protected]>
2025-07-27 18:31 ` Vik Fearing <[email protected]>
2025-07-28 08:41 ` jian he <[email protected]>
2025-07-28 10:47 ` Vik Fearing <[email protected]>
2025-07-29 03:13 ` jian he <[email protected]>
2025-07-29 03:54 ` David G. Johnston <[email protected]>
2025-08-01 08:22 ` jian he <[email protected]>
2025-08-01 10:34 ` Vik Fearing <[email protected]>
2025-08-04 03:10 ` jian he <[email protected]>
2025-08-04 03:36 ` David G. Johnston <[email protected]>
2025-08-04 05:38 ` Corey Huinker <[email protected]>
2025-08-04 05:55 ` David G. Johnston <[email protected]>
2025-08-04 12:57 ` Vik Fearing <[email protected]>
2025-08-12 06:37 ` jian he <[email protected]>
2026-03-23 20:20 ` Corey Huinker <[email protected]>
2026-03-30 08:07 ` jian he <[email protected]>
2026-03-24 06:30 ` Zsolt Parragi <[email protected]>
2026-03-30 08:09 ` jian he <[email protected]>
2026-03-30 22:17 ` Zsolt Parragi <[email protected]>
2026-03-31 07:47 ` jian he <[email protected]>
2026-03-31 17:47 ` Corey Huinker <[email protected]>
2026-06-18 14:51 ` Robert Haas <[email protected]>
2026-06-29 05:19 ` Haibo Yan <[email protected]>
2026-06-29 18:13 ` Robert Haas <[email protected]>
2026-06-30 17:39 ` Haibo Yan <[email protected]>
2026-07-01 13:44 ` jian he <[email protected]>
2026-07-01 16:45 ` Haibo Yan <[email protected]>
2026-07-01 20:58 ` David G. Johnston <[email protected]>
2026-07-02 00:00 ` Haibo Yan <[email protected]>
2026-07-02 03:23 ` jian he <[email protected]>
2026-07-02 04:08 ` David G. Johnston <[email protected]>
2026-07-02 04:14 ` Haibo Yan <[email protected]>
2026-07-02 20:37 ` Robert Haas <[email protected]>
2026-07-02 20:51 ` David G. Johnston <[email protected]>
2026-07-02 21:16 ` Robert Haas <[email protected]>
2026-07-02 21:52 ` David G. Johnston <[email protected]>
2026-07-02 22:23 ` Robert Haas <[email protected]>
2026-07-03 04:08 ` David G. Johnston <[email protected]>
2026-07-03 09:49 ` Robert Haas <[email protected]>
2026-06-29 08:47 ` jian he <[email protected]>
2026-06-29 21:49 ` Zsolt Parragi <[email protected]>
2026-07-02 15:13 ` Peter Eisentraut <[email protected]>
2026-07-02 20:24 ` Robert Haas <[email protected]>
2026-07-04 01:09 ` jian he <[email protected]>
2026-07-09 07:18 ` Peter Eisentraut <[email protected]>
2026-07-06 17:14 ` Haibo Yan <[email protected]>
2026-07-06 18:18 ` Robert Haas <[email protected]>
2026-07-06 20:15 ` Haibo Yan <[email protected]>
2026-07-06 20:27 ` Robert Haas <[email protected]>
2026-07-07 03:56 ` Haibo Yan <[email protected]>
2026-07-07 12:07 ` Robert Haas <[email protected]>
2026-07-09 07:20 ` Peter Eisentraut <[email protected]>
2025-08-04 05:39 ` Corey Huinker <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox