On Mon, Jul 28, 2025 at 6:47 PM Vik Fearing <v...@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?
From c944169304c3922c3cc76166ccd7a54cfcf595ba Mon Sep 17 00:00:00 2001 From: jian he <jian.universal...@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_p6uuyes...@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