From 3da5100614868343b6e6fd2ff395aa3021ba86c4 Mon Sep 17 00:00:00 2001 From: Haibo Yan Date: Wed, 24 Jun 2026 14:52:46 -0700 Subject: [PATCH 2/4] Add pg_format_cast and CREATE/DROP FORMAT CAST Add pg_format_cast, a catalog for registering formatted conversions by source and target type. A format cast function has the signature function(source_type, text) returns target_type where the second argument receives the FORMAT expression coerced to text. Add CREATE FORMAT CAST and DROP FORMAT CAST, syscache support, object-address and dependency handling, and pg_dump support. Format cast objects are separate from pg_cast because formatted casts are always explicit, function-backed, and keyed by a source/target type pair rather than by ordinary cast semantics such as implicit, assignment, binary, or in/out casts. This patch only adds catalog and DDL infrastructure. CAST ... FORMAT execution is added separately. --- doc/src/sgml/catalogs.sgml | 83 +++++++ doc/src/sgml/ref/allfiles.sgml | 2 + doc/src/sgml/ref/alter_extension.sgml | 1 + doc/src/sgml/ref/comment.sgml | 2 + doc/src/sgml/ref/create_format_cast.sgml | 131 +++++++++++ doc/src/sgml/ref/drop_format_cast.sgml | 126 +++++++++++ doc/src/sgml/reference.sgml | 2 + src/backend/catalog/aclchk.c | 2 + src/backend/catalog/dependency.c | 2 + src/backend/catalog/objectaddress.c | 105 +++++++++ src/backend/commands/Makefile | 1 + src/backend/commands/dropcmds.c | 22 ++ src/backend/commands/event_trigger.c | 2 + src/backend/commands/formatcastcmds.c | 245 +++++++++++++++++++++ src/backend/commands/meson.build | 1 + src/backend/commands/seclabel.c | 1 + src/backend/parser/gram.y | 56 ++++- src/backend/tcop/utility.c | 17 ++ src/bin/pg_dump/common.c | 3 + src/bin/pg_dump/pg_dump.c | 166 ++++++++++++++ src/bin/pg_dump/pg_dump.h | 10 + src/bin/pg_dump/pg_dump_sort.c | 9 + src/include/catalog/Makefile | 1 + src/include/catalog/catversion.h | 2 +- src/include/catalog/meson.build | 1 + src/include/catalog/pg_format_cast.h | 62 ++++++ src/include/commands/formatcast.h | 24 ++ src/include/nodes/parsenodes.h | 17 ++ src/include/tcop/cmdtaglist.h | 2 + src/test/regress/expected/format_casts.out | 156 +++++++++++++ src/test/regress/expected/oidjoins.out | 3 + src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/format_casts.sql | 126 +++++++++++ src/tools/pgindent/typedefs.list | 4 + 34 files changed, 1386 insertions(+), 3 deletions(-) create mode 100644 doc/src/sgml/ref/create_format_cast.sgml create mode 100644 doc/src/sgml/ref/drop_format_cast.sgml create mode 100644 src/backend/commands/formatcastcmds.c create mode 100644 src/include/catalog/pg_format_cast.h create mode 100644 src/include/commands/formatcast.h create mode 100644 src/test/regress/expected/format_casts.out create mode 100644 src/test/regress/sql/format_casts.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 4b474c13917..de1a908221a 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -170,6 +170,11 @@ additional foreign table information + + pg_format_cast + format cast functions for formatted casts + + pg_index additional index information @@ -4391,6 +4396,84 @@ SCRAM-SHA-256$<iteration count>:&l + + <structname>pg_format_cast</structname> + + + pg_format_cast + + + + The catalog pg_format_cast stores formatting + conversion functions for formatted casts. A format cast is identified by a + source type and a target type. The associated function is called with the + source value and the FORMAT expression coerced to + text, and returns the target type. At most one format cast + exists for any given pair of source and target types. See for more information. + + + + <structname>pg_format_cast</structname> Columns + + + + + Column Type + + + Description + + + + + + + + oid oid + + + Row identifier + + + + + + fmtsource oid + (references pg_type.oid) + + + OID of the source data type of the formatted cast + + + + + + fmttarget oid + (references pg_type.oid) + + + OID of the target data type of the formatted cast + + + + + + fmtfunc oid + (references pg_proc.oid) + + + OID of the format cast function, which has the signature + function(source_type, text) + returning target_type + + + + +
+
+ + <structname>pg_index</structname> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index e1a56c36221..07f6aff37fc 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -70,6 +70,7 @@ Complete list of usable sgml source files in this directory. + @@ -118,6 +119,7 @@ Complete list of usable sgml source files in this directory. + diff --git a/doc/src/sgml/ref/alter_extension.sgml b/doc/src/sgml/ref/alter_extension.sgml index 60218fcd01c..afc8eba5021 100644 --- a/doc/src/sgml/ref/alter_extension.sgml +++ b/doc/src/sgml/ref/alter_extension.sgml @@ -39,6 +39,7 @@ ALTER EXTENSION name DROP object_name
| FOREIGN DATA WRAPPER object_name | FOREIGN TABLE object_name | + FORMAT CAST (source_type AS target_type) | FUNCTION function_name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ] | MATERIALIZED VIEW object_name | OPERATOR operator_name (left_type, right_type) | diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 6d8479d6829..a99d0887576 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -37,6 +37,7 @@ COMMENT ON EVENT TRIGGER object_name | FOREIGN DATA WRAPPER object_name | FOREIGN TABLE object_name | + FORMAT CAST (source_type AS target_type) | FUNCTION function_name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ] | INDEX object_name | LARGE OBJECT large_object_oid | @@ -336,6 +337,7 @@ COMMENT ON EVENT TRIGGER abort_ddl IS 'Aborts all DDL commands'; COMMENT ON EXTENSION hstore IS 'implements the hstore data type'; COMMENT ON FOREIGN DATA WRAPPER mywrapper IS 'my foreign data wrapper'; COMMENT ON FOREIGN TABLE my_foreign_table IS 'Employee Information in other database'; +COMMENT ON FORMAT CAST (integer AS text) IS 'Allow formatted casts from integer to text'; COMMENT ON FUNCTION my_function (timestamp) IS 'Returns Roman Numeral'; COMMENT ON INDEX my_index IS 'Enforces uniqueness on employee ID'; COMMENT ON LANGUAGE plpython IS 'Python support for stored procedures'; diff --git a/doc/src/sgml/ref/create_format_cast.sgml b/doc/src/sgml/ref/create_format_cast.sgml new file mode 100644 index 00000000000..0e0cf0f7b1b --- /dev/null +++ b/doc/src/sgml/ref/create_format_cast.sgml @@ -0,0 +1,131 @@ + + + + + CREATE FORMAT CAST + + + + CREATE FORMAT CAST + 7 + SQL - Language Statements + + + + CREATE FORMAT CAST + define a new format cast for a formatted cast + + + + +CREATE FORMAT CAST (source_type AS target_type) + WITH FUNCTION function_name [ (argument_type [, ...]) ] + + + + + Description + + + CREATE FORMAT CAST defines a formatting conversion for + CAST(expr AS target_type FORMAT format_expr). + A format cast is selected by the source type and target type of the cast. + Unlike an ordinary cast (see ), a formatted + cast is always explicit and always uses a function, and the function + additionally receives the FORMAT expression. + + + + The format cast function must take exactly two arguments and return the + target type: + +function_name(source_type, text) RETURNS target_type + + The first argument is the value being formatted, and the second argument + receives the FORMAT expression coerced to + text. Only one format cast may be registered for a given + (source_type, + target_type) pair. Neither the source + type nor the target type may be a pseudo-type. + + + + To create a format cast, you must be the owner of the source type or the + target type, and you must have EXECUTE privilege on the + format cast function. + + + + + Parameters + + + + source_type + + + The data type of the value passed to the format cast (the source of the + cast). + + + + + + target_type + + + The data type returned by the format cast (the target of the cast). + + + + + + function_name[(argument_type [, ...])] + + + The function used to perform the formatted cast. The argument list, if + given, must name the two argument types + (source_type and text). + + + + + + + + Examples + + + Register a format cast that converts an integer to + text using the supplied format string: + +CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2; + +CREATE FORMAT CAST (integer AS text) + WITH FUNCTION int4_to_text_fmt(integer, text); + + + + + Compatibility + + + CREATE FORMAT CAST is a + PostgreSQL extension. + + + + + See Also + + + + + + + + diff --git a/doc/src/sgml/ref/drop_format_cast.sgml b/doc/src/sgml/ref/drop_format_cast.sgml new file mode 100644 index 00000000000..8dce02fa882 --- /dev/null +++ b/doc/src/sgml/ref/drop_format_cast.sgml @@ -0,0 +1,126 @@ + + + + + DROP FORMAT CAST + + + + DROP FORMAT CAST + 7 + SQL - Language Statements + + + + DROP FORMAT CAST + remove a format cast + + + + +DROP FORMAT CAST [ IF EXISTS ] (source_type AS target_type) [ CASCADE | RESTRICT ] + + + + + Description + + + DROP FORMAT CAST removes a previously defined format cast for + the given pair of source and target types. + + + + Dropping a format cast requires ownership of either the source type or the + target type. + + + + + Parameters + + + + IF EXISTS + + + Do not throw an error if the format cast does not exist. A notice is + issued in this case. + + + + + + source_type + + + The source data type of the format cast. + + + + + + target_type + + + The target data type of the format cast. + + + + + + CASCADE + + + Automatically drop objects that depend on the format cast, + and in turn all objects that depend on those objects + (see ). + + + + + + RESTRICT + + + Refuse to drop the format cast if any objects depend on it. This is the + default. + + + + + + + + Examples + + + To drop the format cast for the cast from integer to + text: + +DROP FORMAT CAST (integer AS text); + + + + + Compatibility + + + DROP FORMAT CAST is a + PostgreSQL extension. See for details. + + + + + See Also + + + + + + + diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 674ac17e82c..ffe012ce0ed 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -98,6 +98,7 @@ &createExtension; &createForeignDataWrapper; &createForeignTable; + &createFormatCast; &createFunction; &createGroup; &createIndex; @@ -146,6 +147,7 @@ &dropExtension; &dropForeignDataWrapper; &dropForeignTable; + &dropFormatCast; &dropFunction; &dropGroup; &dropIndex; diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..267cbaef9a4 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -2794,6 +2794,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_AMPROC: case OBJECT_ATTRIBUTE: case OBJECT_CAST: + case OBJECT_FORMAT_CAST: case OBJECT_DEFAULT: case OBJECT_DEFACL: case OBJECT_DOMCONSTRAINT: @@ -2937,6 +2938,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_AMPROC: case OBJECT_ATTRIBUTE: case OBJECT_CAST: + case OBJECT_FORMAT_CAST: case OBJECT_DEFAULT: case OBJECT_DEFACL: case OBJECT_DOMCONSTRAINT: diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index c54774b3275..ff7ab606bcc 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -41,6 +41,7 @@ #include "catalog/pg_extension.h" #include "catalog/pg_foreign_data_wrapper.h" #include "catalog/pg_foreign_server.h" +#include "catalog/pg_format_cast.h" #include "catalog/pg_init_privs.h" #include "catalog/pg_language.h" #include "catalog/pg_largeobject.h" @@ -1534,6 +1535,7 @@ doDeletion(const ObjectAddress *object, int flags) case DefaultAclRelationId: case EventTriggerRelationId: case TransformRelationId: + case FormatCastRelationId: case AuthMemRelationId: DropObjectById(object); break; diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index af0e4703616..1d84be82e0d 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -37,6 +37,7 @@ #include "catalog/pg_extension.h" #include "catalog/pg_foreign_data_wrapper.h" #include "catalog/pg_foreign_server.h" +#include "catalog/pg_format_cast.h" #include "catalog/pg_language.h" #include "catalog/pg_largeobject.h" #include "catalog/pg_largeobject_metadata.h" @@ -70,6 +71,7 @@ #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" +#include "commands/formatcast.h" #include "commands/policy.h" #include "commands/proclang.h" #include "commands/tablespace.h" @@ -179,6 +181,20 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_CAST, false }, + { + "format cast", + FormatCastRelationId, + FormatCastOidIndexId, + FORMATCASTOID, + SYSCACHEID_INVALID, + Anum_pg_format_cast_oid, + InvalidAttrNumber, + InvalidAttrNumber, + InvalidAttrNumber, + InvalidAttrNumber, + OBJECT_FORMAT_CAST, + false + }, { "collation", CollationRelationId, @@ -796,6 +812,9 @@ static const struct object_type_map { "cast", OBJECT_CAST }, + { + "format cast", OBJECT_FORMAT_CAST + }, { "collation", OBJECT_COLLATION }, @@ -1165,6 +1184,21 @@ get_object_address(ObjectType objtype, Node *object, address.objectSubId = 0; } break; + case OBJECT_FORMAT_CAST: + { + TypeName *sourcetype = linitial_node(TypeName, castNode(List, object)); + TypeName *targettype = lsecond_node(TypeName, castNode(List, object)); + Oid sourcetypeid; + Oid targettypeid; + + sourcetypeid = LookupTypeNameOid(NULL, sourcetype, missing_ok); + targettypeid = LookupTypeNameOid(NULL, targettype, missing_ok); + address.classId = FormatCastRelationId; + address.objectId = + get_format_cast_oid(sourcetypeid, targettypeid, missing_ok); + address.objectSubId = 0; + } + break; case OBJECT_TRANSFORM: { TypeName *typename = linitial_node(TypeName, castNode(List, object)); @@ -2239,6 +2273,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) * exceptions. */ if (type == OBJECT_TYPE || type == OBJECT_DOMAIN || type == OBJECT_CAST || + type == OBJECT_FORMAT_CAST || type == OBJECT_TRANSFORM || type == OBJECT_DOMCONSTRAINT) { Datum *elems; @@ -2291,6 +2326,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) type == OBJECT_ROUTINE || type == OBJECT_OPERATOR || type == OBJECT_CAST || + type == OBJECT_FORMAT_CAST || type == OBJECT_AMOP || type == OBJECT_AMPROC) { @@ -2336,6 +2372,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) pg_fallthrough; case OBJECT_DOMCONSTRAINT: case OBJECT_CAST: + case OBJECT_FORMAT_CAST: case OBJECT_PUBLICATION_REL: case OBJECT_DEFACL: case OBJECT_TRANSFORM: @@ -2424,6 +2461,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) objnode = (Node *) typename; break; case OBJECT_CAST: + case OBJECT_FORMAT_CAST: case OBJECT_DOMCONSTRAINT: case OBJECT_TRANSFORM: objnode = (Node *) list_make2(typename, linitial(args)); @@ -2583,6 +2621,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, address.objectId))); break; case OBJECT_CAST: + case OBJECT_FORMAT_CAST: { /* We can only check permissions on the source/target types */ TypeName *sourcetype = linitial_node(TypeName, castNode(List, object)); @@ -3111,6 +3150,31 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case FormatCastRelationId: + { + HeapTuple fmtTup; + Form_pg_format_cast fmtForm; + + fmtTup = SearchSysCache1(FORMATCASTOID, + ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(fmtTup)) + { + if (!missing_ok) + elog(ERROR, "could not find tuple for format cast %u", + object->objectId); + break; + } + + fmtForm = (Form_pg_format_cast) GETSTRUCT(fmtTup); + + appendStringInfo(&buffer, _("format cast from %s to %s"), + format_type_be(fmtForm->fmtsource), + format_type_be(fmtForm->fmttarget)); + + ReleaseSysCache(fmtTup); + break; + } + case CollationRelationId: { HeapTuple collTup; @@ -4762,6 +4826,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "cast"); break; + case FormatCastRelationId: + appendStringInfoString(&buffer, "format cast"); + break; + case CollationRelationId: appendStringInfoString(&buffer, "collation"); break; @@ -5225,6 +5293,43 @@ getObjectIdentityParts(const ObjectAddress *object, break; } + case FormatCastRelationId: + { + Relation fmtRel; + HeapTuple tup; + Form_pg_format_cast fmtForm; + + fmtRel = table_open(FormatCastRelationId, AccessShareLock); + + tup = get_catalog_object_by_oid(fmtRel, Anum_pg_format_cast_oid, + object->objectId); + + if (!HeapTupleIsValid(tup)) + { + if (!missing_ok) + elog(ERROR, "could not find tuple for format cast %u", + object->objectId); + + table_close(fmtRel, AccessShareLock); + break; + } + + fmtForm = (Form_pg_format_cast) GETSTRUCT(tup); + + appendStringInfo(&buffer, "(%s AS %s)", + format_type_be_qualified(fmtForm->fmtsource), + format_type_be_qualified(fmtForm->fmttarget)); + + if (objname) + { + *objname = list_make1(format_type_be_qualified(fmtForm->fmtsource)); + *objargs = list_make1(format_type_be_qualified(fmtForm->fmttarget)); + } + + table_close(fmtRel, AccessShareLock); + break; + } + case CollationRelationId: { HeapTuple collTup; diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index 5b9d084977e..b6928dd6014 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -38,6 +38,7 @@ OBJS = \ explain_state.o \ extension.o \ foreigncmds.o \ + formatcastcmds.o \ functioncmds.o \ indexcmds.o \ lockcmds.o \ diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index 88a2df65c69..776359dfdbf 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -25,6 +25,7 @@ #include "miscadmin.h" #include "parser/parse_type.h" #include "utils/acl.h" +#include "utils/builtins.h" #include "utils/lsyscache.h" @@ -401,6 +402,27 @@ does_not_exist_skipping(ObjectType objtype, Node *object) } } break; + case OBJECT_FORMAT_CAST: + { + if (!type_in_list_does_not_exist_skipping(list_make1(linitial(castNode(List, object))), &msg, &name) && + !type_in_list_does_not_exist_skipping(list_make1(lsecond(castNode(List, object))), &msg, &name)) + { + /* + * Both types exist (else the checks above would have + * produced a message), so resolve them to OIDs and report + * them with format_type_be(). This keeps the wording + * consistent with the duplicate-object and undefined-object + * errors, which also use format_type_be(). + */ + Oid sourcetypeid = typenameTypeId(NULL, linitial_node(TypeName, castNode(List, object))); + Oid targettypeid = typenameTypeId(NULL, lsecond_node(TypeName, castNode(List, object))); + + msg = gettext_noop("format cast from type %s to type %s does not exist, skipping"); + name = format_type_be(sourcetypeid); + args = format_type_be(targettypeid); + } + } + break; case OBJECT_TRANSFORM: if (!type_in_list_does_not_exist_skipping(list_make1(linitial(castNode(List, object))), &msg, &name)) { diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index adc6eabc0f4..47a05a77122 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2301,6 +2301,7 @@ stringify_grant_objtype(ObjectType objtype) case OBJECT_AMPROC: case OBJECT_ATTRIBUTE: case OBJECT_CAST: + case OBJECT_FORMAT_CAST: case OBJECT_COLLATION: case OBJECT_CONVERSION: case OBJECT_DEFAULT: @@ -2385,6 +2386,7 @@ stringify_adefprivs_objtype(ObjectType objtype) case OBJECT_AMPROC: case OBJECT_ATTRIBUTE: case OBJECT_CAST: + case OBJECT_FORMAT_CAST: case OBJECT_COLLATION: case OBJECT_CONVERSION: case OBJECT_DEFAULT: diff --git a/src/backend/commands/formatcastcmds.c b/src/backend/commands/formatcastcmds.c new file mode 100644 index 00000000000..bbee0e74492 --- /dev/null +++ b/src/backend/commands/formatcastcmds.c @@ -0,0 +1,245 @@ +/*------------------------------------------------------------------------- + * + * formatcastcmds.c + * Routines for SQL commands that manipulate format casts. + * + * A format cast associates a function with a (source type, target type) pair. + * The function implements CAST(expr AS target FORMAT format_expr): it + * receives the source value and the FORMAT expression (passed as text) and + * returns the target type. Format casts are registered in the pg_format_cast + * catalog and are intentionally separate from pg_cast (see pg_format_cast.h). + * + * This file provides the catalog registration machinery (CREATE/DROP + * FORMAT CAST). It does not perform expression transformation or execution of + * formatted casts. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/formatcastcmds.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/htup_details.h" +#include "access/table.h" +#include "catalog/catalog.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/objectaccess.h" +#include "catalog/objectaddress.h" +#include "catalog/pg_format_cast.h" +#include "catalog/pg_proc.h" +#include "catalog/pg_type.h" +#include "commands/formatcast.h" +#include "miscadmin.h" +#include "parser/parse_func.h" +#include "parser/parse_type.h" +#include "utils/acl.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/rel.h" +#include "utils/syscache.h" + +/* + * Validate the signature of a format cast function: it must be a normal + * function (not a procedure, aggregate or window function), must not return + * a set, and must have the signature + * + * function(source_type, text) returns target_type + * + * NB: the format cast signature is fixed at two arguments and does not include + * the target type modifier; typmod-aware format cast signatures are not + * supported. + */ +static void +check_format_cast_function(Form_pg_proc procstruct, + Oid sourcetypeid, Oid targettypeid) +{ + if (procstruct->prokind != PROKIND_FUNCTION) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("format cast function must be a normal function"))); + if (procstruct->proretset) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("format cast function must not return a set"))); + if (procstruct->pronargs != 2) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("format cast function must take exactly two arguments"))); + if (procstruct->proargtypes.values[0] != sourcetypeid) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("first argument of format cast function must be type %s", + format_type_be(sourcetypeid)))); + if (procstruct->proargtypes.values[1] != TEXTOID) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("second argument of format cast function must be type %s", + "text"))); + if (procstruct->prorettype != targettypeid) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("return data type of format cast function must be type %s", + format_type_be(targettypeid)))); +} + +/* + * CREATE FORMAT CAST + */ +ObjectAddress +CreateFormatCast(CreateFormatCastStmt *stmt) +{ + Oid sourcetypeid; + Oid targettypeid; + char sourcetyptype; + char targettyptype; + Oid funcid; + AclResult aclresult; + Form_pg_proc procstruct; + HeapTuple tuple; + HeapTuple newtuple; + Datum values[Natts_pg_format_cast]; + bool nulls[Natts_pg_format_cast] = {0}; + Oid formatcastid; + Relation relation; + ObjectAddress myself, + referenced; + ObjectAddresses *addrs; + + sourcetypeid = typenameTypeId(NULL, stmt->sourcetype); + targettypeid = typenameTypeId(NULL, stmt->targettype); + sourcetyptype = get_typtype(sourcetypeid); + targettyptype = get_typtype(targettypeid); + + /* No pseudo-types allowed */ + if (sourcetyptype == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("source data type %s is a pseudo-type", + TypeNameToString(stmt->sourcetype)))); + if (targettyptype == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("target data type %s is a pseudo-type", + TypeNameToString(stmt->targettype)))); + + /* + * Permission check. As for CREATE CAST, the caller must own at least one + * of the two types involved; owning a type is what authorizes defining + * conversion behavior for it. In addition, and following CREATE OPERATOR + * and CREATE AGGREGATE, we require EXECUTE permission on the format cast + * function (this will also be checked when the format cast is used, but it + * is a good idea to verify it up front). We intentionally do not require + * ownership of the function, unlike CREATE TRANSFORM, because a format cast + * is a data conversion rather than a procedural-language binding. + */ + if (!object_ownercheck(TypeRelationId, sourcetypeid, GetUserId()) + && !object_ownercheck(TypeRelationId, targettypeid, GetUserId())) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be owner of type %s or type %s", + format_type_be(sourcetypeid), + format_type_be(targettypeid)))); + + /* + * Look up and validate the format cast function. + */ + funcid = LookupFuncWithArgs(OBJECT_FUNCTION, stmt->func, false); + + aclresult = object_aclcheck(ProcedureRelationId, funcid, GetUserId(), ACL_EXECUTE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_FUNCTION, + NameListToString(stmt->func->objname)); + + tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for function %u", funcid); + procstruct = (Form_pg_proc) GETSTRUCT(tuple); + check_format_cast_function(procstruct, sourcetypeid, targettypeid); + ReleaseSysCache(tuple); + + /* + * Check for a pre-existing format cast for this (source, target) pair. For + * this version only one format cast per pair is allowed. + */ + relation = table_open(FormatCastRelationId, RowExclusiveLock); + + if (SearchSysCacheExists2(FORMATCASTSOURCETARGET, + ObjectIdGetDatum(sourcetypeid), + ObjectIdGetDatum(targettypeid))) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("format cast from type %s to type %s already exists", + format_type_be(sourcetypeid), + format_type_be(targettypeid)))); + + /* + * Build and insert the catalog tuple. + */ + formatcastid = GetNewOidWithIndex(relation, FormatCastOidIndexId, + Anum_pg_format_cast_oid); + values[Anum_pg_format_cast_oid - 1] = ObjectIdGetDatum(formatcastid); + values[Anum_pg_format_cast_fmtsource - 1] = ObjectIdGetDatum(sourcetypeid); + values[Anum_pg_format_cast_fmttarget - 1] = ObjectIdGetDatum(targettypeid); + values[Anum_pg_format_cast_fmtfunc - 1] = ObjectIdGetDatum(funcid); + + newtuple = heap_form_tuple(RelationGetDescr(relation), values, nulls); + CatalogTupleInsert(relation, newtuple); + + addrs = new_object_addresses(); + + ObjectAddressSet(myself, FormatCastRelationId, formatcastid); + + /* dependency on source type */ + ObjectAddressSet(referenced, TypeRelationId, sourcetypeid); + add_exact_object_address(&referenced, addrs); + + /* dependency on target type */ + ObjectAddressSet(referenced, TypeRelationId, targettypeid); + add_exact_object_address(&referenced, addrs); + + /* dependency on the format cast function */ + ObjectAddressSet(referenced, ProcedureRelationId, funcid); + add_exact_object_address(&referenced, addrs); + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + free_object_addresses(addrs); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + /* Post creation hook for new format cast */ + InvokeObjectPostCreateHook(FormatCastRelationId, formatcastid, 0); + + heap_freetuple(newtuple); + + table_close(relation, RowExclusiveLock); + + return myself; +} + +/* + * get_format_cast_oid - given source and target type OIDs, look up a + * format cast OID + */ +Oid +get_format_cast_oid(Oid sourcetypeid, Oid targettypeid, bool missing_ok) +{ + Oid oid; + + oid = GetSysCacheOid2(FORMATCASTSOURCETARGET, Anum_pg_format_cast_oid, + ObjectIdGetDatum(sourcetypeid), + ObjectIdGetDatum(targettypeid)); + if (!OidIsValid(oid) && !missing_ok) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("format cast from type %s to type %s does not exist", + format_type_be(sourcetypeid), + format_type_be(targettypeid)))); + return oid; +} diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index 9f258d566eb..689ad0c16db 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -26,6 +26,7 @@ backend_sources += files( 'explain_state.c', 'extension.c', 'foreigncmds.c', + 'formatcastcmds.c', 'functioncmds.c', 'indexcmds.c', 'lockcmds.c', diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index 77542d04200..f57a6d6acc2 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -66,6 +66,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_AMPROC: case OBJECT_ATTRIBUTE: case OBJECT_CAST: + case OBJECT_FORMAT_CAST: case OBJECT_COLLATION: case OBJECT_CONVERSION: case OBJECT_DEFAULT: diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index ef4881efc81..8ef4d22ecba 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -295,13 +295,14 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt + CreateFormatCastStmt CreatePropGraphStmt AlterPropGraphStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt DropOpClassStmt DropOpFamilyStmt DropStmt DropCastStmt DropRoleStmt DropdbStmt DropTableSpaceStmt - DropTransformStmt + DropTransformStmt DropFormatCastStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt @@ -1105,6 +1106,7 @@ stmt: | CreateStatsStmt | CreateTableSpaceStmt | CreateTransformStmt + | CreateFormatCastStmt | CreateTrigStmt | CreateEventTrigStmt | CreateRoleStmt @@ -1125,6 +1127,7 @@ stmt: | DropSubscriptionStmt | DropTableSpaceStmt | DropTransformStmt + | DropFormatCastStmt | DropRoleStmt | DropUserMappingStmt | DropdbStmt @@ -5522,6 +5525,16 @@ AlterExtensionContentsStmt: n->object = (Node *) list_make2($7, $9); $$ = (Node *) n; } + | ALTER EXTENSION name add_drop FORMAT CAST '(' Typename AS Typename ')' + { + AlterExtensionContentsStmt *n = makeNode(AlterExtensionContentsStmt); + + n->extname = $3; + n->action = $4; + n->objtype = OBJECT_FORMAT_CAST; + n->object = (Node *) list_make2($8, $10); + $$ = (Node *) n; + } | ALTER EXTENSION name add_drop DOMAIN_P Typename { AlterExtensionContentsStmt *n = makeNode(AlterExtensionContentsStmt); @@ -7512,6 +7525,15 @@ CommentStmt: n->comment = $10; $$ = (Node *) n; } + | COMMENT ON FORMAT CAST '(' Typename AS Typename ')' IS comment_text + { + CommentStmt *n = makeNode(CommentStmt); + + n->objtype = OBJECT_FORMAT_CAST; + n->object = (Node *) list_make2($6, $8); + n->comment = $11; + $$ = (Node *) n; + } ; comment_text: @@ -9893,6 +9915,38 @@ DropTransformStmt: DROP TRANSFORM opt_if_exists FOR Typename LANGUAGE name opt_d ; +/***************************************************************************** + * + * CREATE FORMAT CAST / DROP FORMAT CAST + * + * A format cast registers the function implementing + * CAST(expr AS target FORMAT format_expr) for a (source, target) type pair. + *****************************************************************************/ + +CreateFormatCastStmt: CREATE FORMAT CAST '(' Typename AS Typename ')' WITH FUNCTION function_with_argtypes + { + CreateFormatCastStmt *n = makeNode(CreateFormatCastStmt); + + n->sourcetype = $5; + n->targettype = $7; + n->func = $11; + $$ = (Node *) n; + } + ; + +DropFormatCastStmt: DROP FORMAT CAST opt_if_exists '(' Typename AS Typename ')' opt_drop_behavior + { + DropStmt *n = makeNode(DropStmt); + + n->removeType = OBJECT_FORMAT_CAST; + n->objects = list_make1(list_make2($6, $8)); + n->behavior = $10; + n->missing_ok = $4; + $$ = (Node *) n; + } + ; + + /***************************************************************************** * * QUERY: diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..5ecea23458f 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -37,6 +37,7 @@ #include "commands/event_trigger.h" #include "commands/explain.h" #include "commands/extension.h" +#include "commands/formatcast.h" #include "commands/lockcmds.h" #include "commands/matview.h" #include "commands/policy.h" @@ -193,6 +194,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateTableAsStmt: case T_CreateTableSpaceStmt: case T_CreateTransformStmt: + case T_CreateFormatCastStmt: case T_CreateTrigStmt: case T_CreateUserMappingStmt: case T_CreatedbStmt: @@ -1755,6 +1757,10 @@ ProcessUtilitySlow(ParseState *pstate, address = CreateTransform((CreateTransformStmt *) parsetree); break; + case T_CreateFormatCastStmt: + address = CreateFormatCast((CreateFormatCastStmt *) parsetree); + break; + case T_AlterOpFamilyStmt: AlterOpFamily((AlterOpFamilyStmt *) parsetree); /* commands are stashed in AlterOpFamily */ @@ -2666,6 +2672,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_TRANSFORM: tag = CMDTAG_DROP_TRANSFORM; break; + case OBJECT_FORMAT_CAST: + tag = CMDTAG_DROP_FORMAT_CAST; + break; case OBJECT_ACCESS_METHOD: tag = CMDTAG_DROP_ACCESS_METHOD; break; @@ -2981,6 +2990,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_CREATE_TRANSFORM; break; + case T_CreateFormatCastStmt: + tag = CMDTAG_CREATE_FORMAT_CAST; + break; + case T_CreateTrigStmt: tag = CMDTAG_CREATE_TRIGGER; break; @@ -3690,6 +3703,10 @@ GetCommandLogLevel(Node *parsetree) lev = LOGSTMT_DDL; break; + case T_CreateFormatCastStmt: + lev = LOGSTMT_DDL; + break; + case T_AlterOpFamilyStmt: lev = LOGSTMT_DDL; break; diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index dc98c5c5c09..54cf9c07f68 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -188,6 +188,9 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading transforms"); getTransforms(fout); + pg_log_info("reading format casts"); + getFormatCasts(fout); + pg_log_info("reading table inheritance information"); inhinfo = getInherits(fout, &numInherits); diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index c56437d6057..6517a450daa 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -300,6 +300,7 @@ static void dumpProcLang(Archive *fout, const ProcLangInfo *plang); static void dumpFunc(Archive *fout, const FuncInfo *finfo); static void dumpCast(Archive *fout, const CastInfo *cast); static void dumpTransform(Archive *fout, const TransformInfo *transform); +static void dumpFormatCast(Archive *fout, const FormatCastInfo *formatcast); static void dumpOpr(Archive *fout, const OprInfo *oprinfo); static void dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo); static void dumpOpclass(Archive *fout, const OpclassInfo *opcinfo); @@ -9320,6 +9321,83 @@ getTransforms(Archive *fout) destroyPQExpBuffer(query); } +/* + * getFormatCasts + * get basic information about every format cast in the system + */ +void +getFormatCasts(Archive *fout) +{ + PGresult *res; + int ntups; + int i; + PQExpBuffer query; + FormatCastInfo *formatcastinfo; + int i_tableoid; + int i_oid; + int i_fmtsource; + int i_fmttarget; + int i_fmtfunc; + + /* Format casts were introduced in v19 */ + if (fout->remoteVersion < 190000) + return; + + query = createPQExpBuffer(); + + appendPQExpBufferStr(query, "SELECT tableoid, oid, " + "fmtsource, fmttarget, fmtfunc " + "FROM pg_format_cast " + "ORDER BY 3,4"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + formatcastinfo = pg_malloc_array(FormatCastInfo, ntups); + + i_tableoid = PQfnumber(res, "tableoid"); + i_oid = PQfnumber(res, "oid"); + i_fmtsource = PQfnumber(res, "fmtsource"); + i_fmttarget = PQfnumber(res, "fmttarget"); + i_fmtfunc = PQfnumber(res, "fmtfunc"); + + for (i = 0; i < ntups; i++) + { + PQExpBufferData namebuf; + TypeInfo *sTypeInfo; + TypeInfo *tTypeInfo; + + formatcastinfo[i].dobj.objType = DO_FORMAT_CAST; + formatcastinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid)); + formatcastinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&formatcastinfo[i].dobj); + formatcastinfo[i].fmtsource = atooid(PQgetvalue(res, i, i_fmtsource)); + formatcastinfo[i].fmttarget = atooid(PQgetvalue(res, i, i_fmttarget)); + formatcastinfo[i].fmtfunc = atooid(PQgetvalue(res, i, i_fmtfunc)); + + /* + * Try to name the format cast as a concatenation of the type names. + * This is only used for purposes of sorting. If we fail to find + * either type, the name will be an empty string. + */ + initPQExpBuffer(&namebuf); + sTypeInfo = findTypeByOid(formatcastinfo[i].fmtsource); + tTypeInfo = findTypeByOid(formatcastinfo[i].fmttarget); + if (sTypeInfo && tTypeInfo) + appendPQExpBuffer(&namebuf, "%s %s", + sTypeInfo->dobj.name, tTypeInfo->dobj.name); + formatcastinfo[i].dobj.name = namebuf.data; + + /* Decide whether we want to dump it */ + selectDumpableObject(&(formatcastinfo[i].dobj), fout); + } + + PQclear(res); + + destroyPQExpBuffer(query); +} + /* * getTableAttrs - * for each interesting table, read info about its attributes @@ -11913,6 +11991,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_TRANSFORM: dumpTransform(fout, (const TransformInfo *) dobj); break; + case DO_FORMAT_CAST: + dumpFormatCast(fout, (const FormatCastInfo *) dobj); + break; case DO_SEQUENCE_SET: dumpSequenceData(fout, (const TableDataInfo *) dobj); break; @@ -14255,6 +14336,90 @@ dumpTransform(Archive *fout, const TransformInfo *transform) destroyPQExpBuffer(transformargs); } +/* + * Dump a format cast + */ +static void +dumpFormatCast(Archive *fout, const FormatCastInfo *formatcast) +{ + DumpOptions *dopt = fout->dopt; + PQExpBuffer defqry; + PQExpBuffer delqry; + PQExpBuffer labelq; + PQExpBuffer formatcastargs; + FuncInfo *funcInfo; + const char *sourceType; + const char *targetType; + + /* Do nothing if not dumping schema */ + if (!dopt->dumpSchema) + return; + + /* Cannot dump if we don't have the format cast function's info */ + funcInfo = findFuncByOid(formatcast->fmtfunc); + if (funcInfo == NULL) + pg_fatal("could not find function definition for function with OID %u", + formatcast->fmtfunc); + + defqry = createPQExpBuffer(); + delqry = createPQExpBuffer(); + labelq = createPQExpBuffer(); + formatcastargs = createPQExpBuffer(); + + sourceType = getFormattedTypeName(fout, formatcast->fmtsource, zeroAsNone); + targetType = getFormattedTypeName(fout, formatcast->fmttarget, zeroAsNone); + + appendPQExpBuffer(delqry, "DROP FORMAT CAST (%s AS %s);\n", + sourceType, targetType); + + appendPQExpBuffer(defqry, "CREATE FORMAT CAST (%s AS %s) ", + sourceType, targetType); + + { + char *fsig = format_function_signature(fout, funcInfo, true); + + /* + * Always qualify the function name (format_function_signature won't + * qualify it). + */ + appendPQExpBuffer(defqry, "WITH FUNCTION %s.%s", + fmtId(funcInfo->dobj.namespace->dobj.name), fsig); + free(fsig); + } + appendPQExpBufferStr(defqry, ";\n"); + + appendPQExpBuffer(labelq, "FORMAT CAST (%s AS %s)", + sourceType, targetType); + + appendPQExpBuffer(formatcastargs, "(%s AS %s)", + sourceType, targetType); + + if (dopt->binary_upgrade) + binary_upgrade_extension_member(defqry, &formatcast->dobj, + "FORMAT CAST", formatcastargs->data, NULL); + + if (formatcast->dobj.dump & DUMP_COMPONENT_DEFINITION) + ArchiveEntry(fout, formatcast->dobj.catId, formatcast->dobj.dumpId, + ARCHIVE_OPTS(.tag = labelq->data, + .description = "FORMAT CAST", + .section = SECTION_PRE_DATA, + .createStmt = defqry->data, + .dropStmt = delqry->data, + .deps = formatcast->dobj.dependencies, + .nDeps = formatcast->dobj.nDeps)); + + /* Dump Format cast Comments */ + if (formatcast->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, "FORMAT CAST", formatcastargs->data, + NULL, "", + formatcast->dobj.catId, 0, formatcast->dobj.dumpId); + + destroyPQExpBuffer(defqry); + destroyPQExpBuffer(delqry); + destroyPQExpBuffer(labelq); + destroyPQExpBuffer(formatcastargs); +} + /* * dumpOpr @@ -20696,6 +20861,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_FDW: case DO_FOREIGN_SERVER: case DO_TRANSFORM: + case DO_FORMAT_CAST: /* Pre-data objects: must come before the pre-data boundary */ addObjectDependency(preDataBound, dobj->dumpId); break; diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 5a6726d8b12..c767185ec15 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -73,6 +73,7 @@ typedef enum DO_FOREIGN_SERVER, DO_DEFAULT_ACL, DO_TRANSFORM, + DO_FORMAT_CAST, DO_LARGE_OBJECT, DO_LARGE_OBJECT_DATA, DO_PRE_DATA_BOUNDARY, @@ -559,6 +560,14 @@ typedef struct _transformInfo Oid trftosql; } TransformInfo; +typedef struct _formatCastInfo +{ + DumpableObject dobj; + Oid fmtsource; + Oid fmttarget; + Oid fmtfunc; +} FormatCastInfo; + /* InhInfo isn't a DumpableObject, just temporary state */ typedef struct _inhInfo { @@ -814,6 +823,7 @@ extern void getTriggers(Archive *fout, TableInfo tblinfo[], int numTables); extern void getProcLangs(Archive *fout); extern void getCasts(Archive *fout); extern void getTransforms(Archive *fout); +extern void getFormatCasts(Archive *fout); extern void getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables); extern bool shouldPrintColumn(const DumpOptions *dopt, const TableInfo *tbinfo, int colno); extern void getTSParsers(Archive *fout); diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index 03e5c1c1116..3603f5f5fe3 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -60,6 +60,7 @@ enum dbObjectTypePriorities PRIO_EXTENSION, PRIO_TYPE, /* used for DO_TYPE and DO_SHELL_TYPE */ PRIO_CAST, + PRIO_FORMAT_CAST, PRIO_FUNC, PRIO_AGG, PRIO_ACCESS_METHOD, @@ -128,6 +129,7 @@ static const int dbObjectTypePriority[] = [DO_FK_CONSTRAINT] = PRIO_FK_CONSTRAINT, [DO_PROCLANG] = PRIO_PROCLANG, [DO_CAST] = PRIO_CAST, + [DO_FORMAT_CAST] = PRIO_FORMAT_CAST, [DO_TABLE_DATA] = PRIO_TABLE_DATA, [DO_SEQUENCE_SET] = PRIO_SEQUENCE_SET, [DO_DUMMY_TYPE] = PRIO_DUMMY_TYPE, @@ -1649,6 +1651,13 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) ((CastInfo *) obj)->casttarget, obj->dumpId, obj->catId.oid); return; + case DO_FORMAT_CAST: + snprintf(buf, bufsize, + "FORMAT CAST %u to %u (ID %d OID %u)", + ((FormatCastInfo *) obj)->fmtsource, + ((FormatCastInfo *) obj)->fmttarget, + obj->dumpId, obj->catId.oid); + return; case DO_TRANSFORM: snprintf(buf, bufsize, "TRANSFORM %u lang %u (ID %d OID %u)", diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index bab57372b88..8397cf0756a 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -75,6 +75,7 @@ CATALOG_HEADERS := \ pg_parameter_acl.h \ pg_partitioned_table.h \ pg_range.h \ + pg_format_cast.h \ pg_transform.h \ pg_sequence.h \ pg_publication.h \ diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 635c0d9cb13..6402cc76c87 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202606281 +#define CATALOG_VERSION_NO 202606282 #endif diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index fa836e4ee25..3cb63f85dd2 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -62,6 +62,7 @@ catalog_headers = [ 'pg_parameter_acl.h', 'pg_partitioned_table.h', 'pg_range.h', + 'pg_format_cast.h', 'pg_transform.h', 'pg_sequence.h', 'pg_publication.h', diff --git a/src/include/catalog/pg_format_cast.h b/src/include/catalog/pg_format_cast.h new file mode 100644 index 00000000000..f4647d82aa0 --- /dev/null +++ b/src/include/catalog/pg_format_cast.h @@ -0,0 +1,62 @@ +/*------------------------------------------------------------------------- + * + * pg_format_cast.h + * definition of the "format cast" system catalog (pg_format_cast) + * + * A format cast registers a function that implements + * CAST(expr AS target FORMAT format_expr) for a particular + * (source type, target type) pair. The format cast function receives the + * source value and the FORMAT expression (as text) and returns the target + * type. This is intentionally separate from pg_cast: ordinary casts have + * implicit/assignment/explicit contexts and binary-coercible/WITH INOUT/ + * WITHOUT FUNCTION methods, whereas a formatted cast is always explicit and + * always requires a function. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_format_cast.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_FORMAT_CAST_H +#define PG_FORMAT_CAST_H + +#include "catalog/genbki.h" +#include "catalog/pg_format_cast_d.h" /* IWYU pragma: export */ + +/* ---------------- + * pg_format_cast definition. cpp turns this into + * typedef struct FormData_pg_format_cast + * ---------------- + */ +BEGIN_CATALOG_STRUCT + +CATALOG(pg_format_cast,8647,FormatCastRelationId) +{ + Oid oid; /* oid */ + Oid fmtsource BKI_LOOKUP(pg_type); /* source type */ + Oid fmttarget BKI_LOOKUP(pg_type); /* target type */ + Oid fmtfunc BKI_LOOKUP(pg_proc); /* format cast function */ +} FormData_pg_format_cast; + +END_CATALOG_STRUCT + +/* ---------------- + * Form_pg_format_cast corresponds to a pointer to a tuple with + * the format of pg_format_cast relation. + * ---------------- + */ +typedef FormData_pg_format_cast *Form_pg_format_cast; + +DECLARE_UNIQUE_INDEX_PKEY(pg_format_cast_oid_index, 8648, FormatCastOidIndexId, pg_format_cast, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_format_cast_source_target_index, 8649, FormatCastSourceTargetIndexId, pg_format_cast, btree(fmtsource oid_ops, fmttarget oid_ops)); + +MAKE_SYSCACHE(FORMATCASTOID, pg_format_cast_oid_index, 8); +MAKE_SYSCACHE(FORMATCASTSOURCETARGET, pg_format_cast_source_target_index, 8); + +#endif /* PG_FORMAT_CAST_H */ diff --git a/src/include/commands/formatcast.h b/src/include/commands/formatcast.h new file mode 100644 index 00000000000..8ca84a3b8df --- /dev/null +++ b/src/include/commands/formatcast.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * formatcast.h + * prototypes for formatcastcmds.c. + * + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/formatcast.h + * + *------------------------------------------------------------------------- + */ +#ifndef FORMATCAST_H +#define FORMATCAST_H + +#include "catalog/objectaddress.h" +#include "nodes/parsenodes.h" + +extern ObjectAddress CreateFormatCast(CreateFormatCastStmt *stmt); +extern Oid get_format_cast_oid(Oid sourcetypeid, Oid targettypeid, + bool missing_ok); + +#endif /* FORMATCAST_H */ diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 759c6bfae54..71374dab22e 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2445,6 +2445,7 @@ typedef enum ObjectType OBJECT_FDW, OBJECT_FOREIGN_SERVER, OBJECT_FOREIGN_TABLE, + OBJECT_FORMAT_CAST, OBJECT_FUNCTION, OBJECT_INDEX, OBJECT_LANGUAGE, @@ -4356,6 +4357,22 @@ typedef struct CreateTransformStmt ObjectWithArgs *tosql; } CreateTransformStmt; +/* ---------------------- + * CREATE FORMAT CAST Statement + * + * Registers a format cast function for CAST(... AS target FORMAT ...) on a + * (sourcetype, targettype) pair. DROP FORMAT CAST reuses the generic + * DropStmt path with removeType == OBJECT_FORMAT_CAST. + * ---------------------- + */ +typedef struct CreateFormatCastStmt +{ + NodeTag type; + TypeName *sourcetype; /* source data type */ + TypeName *targettype; /* target data type */ + ObjectWithArgs *func; /* format cast function */ +} CreateFormatCastStmt; + /* ---------------------- * PREPARE Statement * ---------------------- diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index befae5f6b4f..c1dda8ab959 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -95,6 +95,7 @@ PG_CMDTAG(CMDTAG_CREATE_EVENT_TRIGGER, "CREATE EVENT TRIGGER", false, false, fal PG_CMDTAG(CMDTAG_CREATE_EXTENSION, "CREATE EXTENSION", true, false, false) PG_CMDTAG(CMDTAG_CREATE_FOREIGN_DATA_WRAPPER, "CREATE FOREIGN DATA WRAPPER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_FOREIGN_TABLE, "CREATE FOREIGN TABLE", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_FORMAT_CAST, "CREATE FORMAT CAST", true, false, false) PG_CMDTAG(CMDTAG_CREATE_FUNCTION, "CREATE FUNCTION", true, false, false) PG_CMDTAG(CMDTAG_CREATE_INDEX, "CREATE INDEX", true, false, false) PG_CMDTAG(CMDTAG_CREATE_LANGUAGE, "CREATE LANGUAGE", true, false, false) @@ -148,6 +149,7 @@ PG_CMDTAG(CMDTAG_DROP_EVENT_TRIGGER, "DROP EVENT TRIGGER", false, false, false) PG_CMDTAG(CMDTAG_DROP_EXTENSION, "DROP EXTENSION", true, false, false) PG_CMDTAG(CMDTAG_DROP_FOREIGN_DATA_WRAPPER, "DROP FOREIGN DATA WRAPPER", true, false, false) PG_CMDTAG(CMDTAG_DROP_FOREIGN_TABLE, "DROP FOREIGN TABLE", true, false, false) +PG_CMDTAG(CMDTAG_DROP_FORMAT_CAST, "DROP FORMAT CAST", true, false, false) PG_CMDTAG(CMDTAG_DROP_FUNCTION, "DROP FUNCTION", true, false, false) PG_CMDTAG(CMDTAG_DROP_INDEX, "DROP INDEX", true, false, false) PG_CMDTAG(CMDTAG_DROP_LANGUAGE, "DROP LANGUAGE", true, false, false) diff --git a/src/test/regress/expected/format_casts.out b/src/test/regress/expected/format_casts.out new file mode 100644 index 00000000000..2ae4ca0d0d0 --- /dev/null +++ b/src/test/regress/expected/format_casts.out @@ -0,0 +1,156 @@ +-- +-- FORMAT CASTS +-- +-- CREATE/DROP FORMAT CAST registers format cast metadata in pg_format_cast, +-- keyed by a (source type, target type) pair. This is catalog and DDL +-- infrastructure only; it does not transform or execute formatted casts. +-- A simple format cast function with the required signature +-- function(source_type, text) returns target_type +CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2; +CREATE FORMAT CAST (integer AS text) + WITH FUNCTION int4_to_text_fmt(integer, text); +-- It shows up in the catalog (use regtype/regprocedure, not raw OIDs) +SELECT fmtsource::regtype, fmttarget::regtype, fmtfunc::regprocedure + FROM pg_format_cast + WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype; + fmtsource | fmttarget | fmtfunc +-----------+-----------+-------------------------------- + integer | text | int4_to_text_fmt(integer,text) +(1 row) + +-- ... and as a first-class object +SELECT pg_describe_object('pg_format_cast'::regclass, oid, 0) + FROM pg_format_cast + WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype; + pg_describe_object +---------------------------------- + format cast from integer to text +(1 row) + +-- Object identity and COMMENT use "FORMAT CAST (source AS target)" (no "FOR CAST"). +SELECT identity FROM pg_identify_object('pg_format_cast'::regclass, + (SELECT oid FROM pg_format_cast + WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype), 0); + identity +------------------------------ + (integer AS pg_catalog.text) +(1 row) + +COMMENT ON FORMAT CAST (integer AS text) IS 'demo format cast'; +SELECT obj_description((SELECT oid FROM pg_format_cast + WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype), + 'pg_format_cast'); + obj_description +------------------ + demo format cast +(1 row) + +COMMENT ON FORMAT CAST (integer AS text) IS NULL; +-- pg_identify_object_as_address() must round-trip back through +-- pg_get_object_address() to the same catalog object. +WITH fmt AS ( + SELECT oid FROM pg_format_cast + WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype +), addr AS ( + SELECT * FROM pg_identify_object_as_address('pg_format_cast'::regclass, + (SELECT oid FROM fmt), 0) +) +SELECT addr.type, addr.object_names, addr.object_args, + (SELECT classid = 'pg_format_cast'::regclass + AND objid = (SELECT oid FROM fmt) + AND objsubid = 0 + FROM pg_get_object_address(addr.type, addr.object_names, addr.object_args)) + AS round_trips + FROM addr; + type | object_names | object_args | round_trips +-------------+--------------+-------------------+------------- + format cast | {integer} | {pg_catalog.text} | t +(1 row) + +-- Only one format cast per (source, target) pair +CREATE FORMAT CAST (integer AS text) + WITH FUNCTION int4_to_text_fmt(integer, text); +ERROR: format cast from type integer to type text already exists +-- Function signature validation +CREATE FUNCTION fmt_bad_nargs(integer) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text; +CREATE FORMAT CAST (integer AS text) + WITH FUNCTION fmt_bad_nargs(integer); -- wrong # of arguments +ERROR: format cast function must take exactly two arguments +CREATE FUNCTION fmt_bad_arg2(integer, integer) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text; +CREATE FORMAT CAST (integer AS text) + WITH FUNCTION fmt_bad_arg2(integer, integer); -- second arg not text +ERROR: second argument of format cast function must be type text +CREATE FUNCTION fmt_bad_arg1(bigint, text) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text; +CREATE FORMAT CAST (integer AS text) + WITH FUNCTION fmt_bad_arg1(bigint, text); -- first arg mismatch +ERROR: first argument of format cast function must be type integer +CREATE FUNCTION fmt_bad_ret(integer, text) RETURNS boolean + LANGUAGE sql IMMUTABLE RETURN true; +CREATE FORMAT CAST (integer AS text) + WITH FUNCTION fmt_bad_ret(integer, text); -- return type mismatch +ERROR: return data type of format cast function must be type text +CREATE FUNCTION fmt_bad_set(integer, text) RETURNS SETOF text + LANGUAGE sql IMMUTABLE AS $$ SELECT $1::text $$; +CREATE FORMAT CAST (integer AS text) + WITH FUNCTION fmt_bad_set(integer, text); -- set-returning rejected +ERROR: format cast function must not return a set +-- No pseudo-types +CREATE FUNCTION fmt_anyel(anyelement, text) RETURNS text + LANGUAGE sql IMMUTABLE AS $$ SELECT $2 $$; +CREATE FORMAT CAST (anyelement AS text) + WITH FUNCTION fmt_anyel(anyelement, text); +ERROR: source data type anyelement is a pseudo-type +-- Registering a format cast does not enable a formatted cast: the FORMAT +-- clause must not be silently ignored or rewritten to a built-in function, +-- so CAST(... FORMAT ...) is rejected during parse analysis. +SELECT CAST(5 AS text FORMAT 'YYYY'); +ERROR: formatted casts are not implemented yet +LINE 1: SELECT CAST(5 AS text FORMAT 'YYYY'); + ^ +DETAIL: No format cast resolution mechanism is available. +-- Dependency behavior: the format cast depends on its function. +DROP FUNCTION int4_to_text_fmt(integer, text); -- fails (RESTRICT) +ERROR: cannot drop function int4_to_text_fmt(integer,text) because other objects depend on it +DETAIL: format cast from integer to text depends on function int4_to_text_fmt(integer,text) +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP FUNCTION int4_to_text_fmt(integer, text) CASCADE; -- drops format cast +NOTICE: drop cascades to format cast from integer to text +SELECT count(*) FROM pg_format_cast + WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype; + count +------- + 0 +(1 row) + +-- Privileges: the creator must own the source or the target type. (The +-- ownership check happens before the function is looked up, so the function +-- name here is irrelevant.) +CREATE ROLE regress_format_cast_user; +SET ROLE regress_format_cast_user; +CREATE FORMAT CAST (text AS integer) + WITH FUNCTION pg_catalog.length(text); +ERROR: must be owner of type text or type integer +RESET ROLE; +DROP ROLE regress_format_cast_user; +-- DROP FORMAT CAST, including IF EXISTS +CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2; +CREATE FORMAT CAST (integer AS text) + WITH FUNCTION int4_to_text_fmt(integer, text); +DROP FORMAT CAST (integer AS text); +DROP FORMAT CAST (integer AS text); -- fails, gone +ERROR: format cast from type integer to type text does not exist +DROP FORMAT CAST IF EXISTS (integer AS text); -- notice, no error +NOTICE: format cast from type integer to type text does not exist, skipping +-- Clean up +DROP FUNCTION int4_to_text_fmt(integer, text); +DROP FUNCTION fmt_bad_nargs(integer); +DROP FUNCTION fmt_bad_arg2(integer, integer); +DROP FUNCTION fmt_bad_arg1(bigint, text); +DROP FUNCTION fmt_bad_ret(integer, text); +DROP FUNCTION fmt_bad_set(integer, text); +DROP FUNCTION fmt_anyel(anyelement, text); diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index d64169b7bf0..cfe3e39770e 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -257,6 +257,9 @@ NOTICE: checking pg_range {rngmltconstruct1} => pg_proc {oid} NOTICE: checking pg_range {rngmltconstruct2} => pg_proc {oid} NOTICE: checking pg_range {rngcanonical} => pg_proc {oid} NOTICE: checking pg_range {rngsubdiff} => pg_proc {oid} +NOTICE: checking pg_format_cast {fmtsource} => pg_type {oid} +NOTICE: checking pg_format_cast {fmttarget} => pg_type {oid} +NOTICE: checking pg_format_cast {fmtfunc} => pg_proc {oid} NOTICE: checking pg_transform {trftype} => pg_type {oid} NOTICE: checking pg_transform {trflang} => pg_language {oid} NOTICE: checking pg_transform {trffromsql} => pg_proc {oid} diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 8fa0a6c47fb..f1bf8f6504b 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi # ---------- # Another group of parallel tests # ---------- -test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse create_property_graph for_portion_of +test: create_aggregate create_function_sql create_cast format_casts constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse create_property_graph for_portion_of # ---------- # sanity_check does a vacuum, affecting the sort order of SELECT * diff --git a/src/test/regress/sql/format_casts.sql b/src/test/regress/sql/format_casts.sql new file mode 100644 index 00000000000..4453a09784e --- /dev/null +++ b/src/test/regress/sql/format_casts.sql @@ -0,0 +1,126 @@ +-- +-- FORMAT CASTS +-- +-- CREATE/DROP FORMAT CAST registers format cast metadata in pg_format_cast, +-- keyed by a (source type, target type) pair. This is catalog and DDL +-- infrastructure only; it does not transform or execute formatted casts. + +-- A simple format cast function with the required signature +-- function(source_type, text) returns target_type +CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2; + +CREATE FORMAT CAST (integer AS text) + WITH FUNCTION int4_to_text_fmt(integer, text); + +-- It shows up in the catalog (use regtype/regprocedure, not raw OIDs) +SELECT fmtsource::regtype, fmttarget::regtype, fmtfunc::regprocedure + FROM pg_format_cast + WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype; + +-- ... and as a first-class object +SELECT pg_describe_object('pg_format_cast'::regclass, oid, 0) + FROM pg_format_cast + WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype; + +-- Object identity and COMMENT use "FORMAT CAST (source AS target)" (no "FOR CAST"). +SELECT identity FROM pg_identify_object('pg_format_cast'::regclass, + (SELECT oid FROM pg_format_cast + WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype), 0); +COMMENT ON FORMAT CAST (integer AS text) IS 'demo format cast'; +SELECT obj_description((SELECT oid FROM pg_format_cast + WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype), + 'pg_format_cast'); +COMMENT ON FORMAT CAST (integer AS text) IS NULL; + +-- pg_identify_object_as_address() must round-trip back through +-- pg_get_object_address() to the same catalog object. +WITH fmt AS ( + SELECT oid FROM pg_format_cast + WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype +), addr AS ( + SELECT * FROM pg_identify_object_as_address('pg_format_cast'::regclass, + (SELECT oid FROM fmt), 0) +) +SELECT addr.type, addr.object_names, addr.object_args, + (SELECT classid = 'pg_format_cast'::regclass + AND objid = (SELECT oid FROM fmt) + AND objsubid = 0 + FROM pg_get_object_address(addr.type, addr.object_names, addr.object_args)) + AS round_trips + FROM addr; + +-- Only one format cast per (source, target) pair +CREATE FORMAT CAST (integer AS text) + WITH FUNCTION int4_to_text_fmt(integer, text); + +-- Function signature validation +CREATE FUNCTION fmt_bad_nargs(integer) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text; +CREATE FORMAT CAST (integer AS text) + WITH FUNCTION fmt_bad_nargs(integer); -- wrong # of arguments + +CREATE FUNCTION fmt_bad_arg2(integer, integer) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text; +CREATE FORMAT CAST (integer AS text) + WITH FUNCTION fmt_bad_arg2(integer, integer); -- second arg not text + +CREATE FUNCTION fmt_bad_arg1(bigint, text) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text; +CREATE FORMAT CAST (integer AS text) + WITH FUNCTION fmt_bad_arg1(bigint, text); -- first arg mismatch + +CREATE FUNCTION fmt_bad_ret(integer, text) RETURNS boolean + LANGUAGE sql IMMUTABLE RETURN true; +CREATE FORMAT CAST (integer AS text) + WITH FUNCTION fmt_bad_ret(integer, text); -- return type mismatch + +CREATE FUNCTION fmt_bad_set(integer, text) RETURNS SETOF text + LANGUAGE sql IMMUTABLE AS $$ SELECT $1::text $$; +CREATE FORMAT CAST (integer AS text) + WITH FUNCTION fmt_bad_set(integer, text); -- set-returning rejected + +-- No pseudo-types +CREATE FUNCTION fmt_anyel(anyelement, text) RETURNS text + LANGUAGE sql IMMUTABLE AS $$ SELECT $2 $$; +CREATE FORMAT CAST (anyelement AS text) + WITH FUNCTION fmt_anyel(anyelement, text); + +-- Registering a format cast does not enable a formatted cast: the FORMAT +-- clause must not be silently ignored or rewritten to a built-in function, +-- so CAST(... FORMAT ...) is rejected during parse analysis. +SELECT CAST(5 AS text FORMAT 'YYYY'); + +-- Dependency behavior: the format cast depends on its function. +DROP FUNCTION int4_to_text_fmt(integer, text); -- fails (RESTRICT) +DROP FUNCTION int4_to_text_fmt(integer, text) CASCADE; -- drops format cast +SELECT count(*) FROM pg_format_cast + WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype; + +-- Privileges: the creator must own the source or the target type. (The +-- ownership check happens before the function is looked up, so the function +-- name here is irrelevant.) +CREATE ROLE regress_format_cast_user; +SET ROLE regress_format_cast_user; +CREATE FORMAT CAST (text AS integer) + WITH FUNCTION pg_catalog.length(text); +RESET ROLE; +DROP ROLE regress_format_cast_user; + +-- DROP FORMAT CAST, including IF EXISTS +CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2; +CREATE FORMAT CAST (integer AS text) + WITH FUNCTION int4_to_text_fmt(integer, text); +DROP FORMAT CAST (integer AS text); +DROP FORMAT CAST (integer AS text); -- fails, gone +DROP FORMAT CAST IF EXISTS (integer AS text); -- notice, no error + +-- Clean up +DROP FUNCTION int4_to_text_fmt(integer, text); +DROP FUNCTION fmt_bad_nargs(integer); +DROP FUNCTION fmt_bad_arg2(integer, integer); +DROP FUNCTION fmt_bad_arg1(bigint, text); +DROP FUNCTION fmt_bad_ret(integer, text); +DROP FUNCTION fmt_bad_set(integer, text); +DROP FUNCTION fmt_anyel(anyelement, text); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index c5db6ca6705..b8f50855e18 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -575,6 +575,7 @@ CreateExtensionStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt +CreateFormatCastStmt CreateFunctionStmt CreateOpClassItem CreateOpClassStmt @@ -921,6 +922,7 @@ FormData_pg_extension FormData_pg_foreign_data_wrapper FormData_pg_foreign_server FormData_pg_foreign_table +FormData_pg_format_cast FormData_pg_index FormData_pg_inherits FormData_pg_language @@ -986,6 +988,7 @@ Form_pg_extension Form_pg_foreign_data_wrapper Form_pg_foreign_server Form_pg_foreign_table +Form_pg_format_cast Form_pg_index Form_pg_inherits Form_pg_language @@ -1027,6 +1030,7 @@ Form_pg_ts_parser Form_pg_ts_template Form_pg_type Form_pg_user_mapping +FormatCastInfo FormatNode FreeBlockNumberArray FreeListData -- 2.54.0