implement CAST(expr AS type FORMAT 'template')
hi.
while working on CAST(... DEFAULT ON ERROR), I came across link[1]https://wiki.postgresql.org/wiki/PostgreSQL_vs_SQL_Standard#Major_features_simply_not_implemented_yet. I don't
have access to the SQL standard, but based on the information in link[1]https://wiki.postgresql.org/wiki/PostgreSQL_vs_SQL_Standard#Major_features_simply_not_implemented_yet, 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
Attachments:
v1-0001-CAST-val-AS-type-FORMAT-template.patchtext/x-patch; charset=US-ASCII; name=v1-0001-CAST-val-AS-type-FORMAT-template.patchDownload
From 8ddb3727f6292d47cdd42e657499fbaabf77f55f Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
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
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
On Mon, Jul 28, 2025 at 2:31 AM Vik Fearing <vik@postgresfriends.org> 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:
v2-0001-CAST-val-AS-type-FORMAT-template.patchtext/x-patch; charset=US-ASCII; name=v2-0001-CAST-val-AS-type-FORMAT-template.patchDownload
From bd4ae95df52319fd2bb50f653cc1ed49884e2ce7 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
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
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
On Mon, Jul 28, 2025 at 6:47 PM Vik Fearing <vik@postgresfriends.org> 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:
v3-0001-CAST-val-AS-type-FORMAT-template.patchtext/x-patch; charset=US-ASCII; name=v3-0001-CAST-val-AS-type-FORMAT-template.patchDownload
From c944169304c3922c3cc76166ccd7a54cfcf595ba Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
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
On Monday, July 28, 2025, jian he <jian.universality@gmail.com> wrote:
On Mon, Jul 28, 2025 at 6:47 PM Vik Fearing <vik@postgresfriends.org>
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.
On Tue, Jul 29, 2025 at 11:54 AM David G. Johnston
<david.g.johnston@gmail.com> 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?
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
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);
On Sun, Aug 3, 2025 at 8:10 PM jian he <jian.universality@gmail.com> 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.
On Sun, Aug 3, 2025 at 11:36 PM David G. Johnston <
david.g.johnston@gmail.com> wrote:
On Sun, Aug 3, 2025 at 8:10 PM jian he <jian.universality@gmail.com>
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?
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.
On Sunday, August 3, 2025, Corey Huinker <corey.huinker@gmail.com> wrote:
On Sun, Aug 3, 2025 at 11:36 PM David G. Johnston <
david.g.johnston@gmail.com> wrote:On Sun, Aug 3, 2025 at 8:10 PM jian he <jian.universality@gmail.com>
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.
On 04/08/2025 07:55, David G. Johnston wrote:
On Sunday, August 3, 2025, Corey Huinker <corey.huinker@gmail.com> wrote:
On Sun, Aug 3, 2025 at 11:36 PM David G. Johnston
<david.g.johnston@gmail.com> 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
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]/messages/by-id/CACJufxF4OW=x2rCwa+ZmcgopDwGKDXha09qTfTpCj3QSTG6Y9Q@mail.gmail.com).
Also for this patch, including these functions in pg_cast is not really
necessary to achieve the intended behavior.
[1]: /messages/by-id/CACJufxF4OW=x2rCwa+ZmcgopDwGKDXha09qTfTpCj3QSTG6Y9Q@mail.gmail.com
Attachments:
v4-0001-CAST-val-AS-type-FORMAT-template.patchtext/x-patch; charset=UTF-8; name=v4-0001-CAST-val-AS-type-FORMAT-template.patchDownload
From ef719a5fcf2e96d7fc756a021aebd8caf5b0b385 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
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