From 70d36e8fd1387233cc01f5aa130bdc8f8d3ccd13 Mon Sep 17 00:00:00 2001 From: Haibo Yan Date: Wed, 24 Jun 2026 14:52:46 -0700 Subject: [PATCH] Add pg_formatter and CREATE/DROP FORMATTER Add pg_formatter, a catalog for registering formatted conversions by source and target type. A formatter function has the signature formatter(source_type, text) returns target_type where the second argument receives the FORMAT expression coerced to text. Add CREATE FORMATTER and DROP FORMATTER, syscache support, object-address and dependency handling, and pg_dump support. Formatter objects are separate from pg_cast because formatted casts are always explicit, function-backed, and keyed by a source/target type pair rather than by ordinary cast semantics such as implicit, assignment, binary, or in/out casts. This patch only adds catalog and DDL infrastructure. CAST ... FORMAT execution is added separately. --- doc/src/sgml/catalogs.sgml | 83 ++++++++ doc/src/sgml/ref/allfiles.sgml | 2 + doc/src/sgml/ref/create_formatter.sgml | 131 ++++++++++++ doc/src/sgml/ref/drop_formatter.sgml | 126 ++++++++++++ doc/src/sgml/reference.sgml | 2 + src/backend/catalog/aclchk.c | 2 + src/backend/catalog/dependency.c | 2 + src/backend/catalog/objectaddress.c | 105 ++++++++++ src/backend/commands/Makefile | 1 + src/backend/commands/dropcmds.c | 22 ++ src/backend/commands/event_trigger.c | 2 + src/backend/commands/formattercmds.c | 245 +++++++++++++++++++++++ src/backend/commands/meson.build | 1 + src/backend/commands/seclabel.c | 1 + src/backend/parser/gram.y | 40 +++- src/backend/tcop/utility.c | 17 ++ src/bin/pg_dump/common.c | 3 + src/bin/pg_dump/pg_dump.c | 166 +++++++++++++++ src/bin/pg_dump/pg_dump.h | 10 + src/bin/pg_dump/pg_dump_sort.c | 9 + src/include/catalog/Makefile | 1 + src/include/catalog/catversion.h | 2 +- src/include/catalog/meson.build | 1 + src/include/catalog/pg_formatter.h | 62 ++++++ src/include/commands/formatter.h | 24 +++ src/include/nodes/parsenodes.h | 17 ++ src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 2 + src/test/regress/expected/formatters.out | 116 +++++++++++ src/test/regress/expected/oidjoins.out | 3 + src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/formatters.sql | 99 +++++++++ src/tools/pgindent/typedefs.list | 4 + 33 files changed, 1300 insertions(+), 4 deletions(-) create mode 100644 doc/src/sgml/ref/create_formatter.sgml create mode 100644 doc/src/sgml/ref/drop_formatter.sgml create mode 100644 src/backend/commands/formattercmds.c create mode 100644 src/include/catalog/pg_formatter.h create mode 100644 src/include/commands/formatter.h create mode 100644 src/test/regress/expected/formatters.out create mode 100644 src/test/regress/sql/formatters.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 4b474c13917..75f4e4840fd 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -170,6 +170,11 @@ additional foreign table information + + pg_formatter + formatter functions for formatted casts + + pg_index additional index information @@ -4391,6 +4396,84 @@ SCRAM-SHA-256$<iteration count>:&l + + <structname>pg_formatter</structname> + + + pg_formatter + + + + The catalog pg_formatter stores formatting + conversion functions for formatted casts. A formatter is identified by a + source type and a target type. The associated function is called with the + source value and the FORMAT expression coerced to + text, and returns the target type. At most one formatter + exists for any given pair of source and target types. See for more information. + + + + <structname>pg_formatter</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 formatter 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..b0bda54d87b 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -70,6 +70,7 @@ Complete list of usable sgml source files in this directory. + @@ -118,6 +119,7 @@ Complete list of usable sgml source files in this directory. + diff --git a/doc/src/sgml/ref/create_formatter.sgml b/doc/src/sgml/ref/create_formatter.sgml new file mode 100644 index 00000000000..268b53e7d2c --- /dev/null +++ b/doc/src/sgml/ref/create_formatter.sgml @@ -0,0 +1,131 @@ + + + + + CREATE FORMATTER + + + + CREATE FORMATTER + 7 + SQL - Language Statements + + + + CREATE FORMATTER + define a new formatter for a formatted cast + + + + +CREATE FORMATTER FOR CAST (source_type AS target_type) + WITH FUNCTION function_name [ (argument_type [, ...]) ] + + + + + Description + + + CREATE FORMATTER defines a formatting conversion for + CAST(expr AS target_type FORMAT format_expr). + A formatter 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 formatter 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 formatter 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 formatter, you must be the owner of the source type or the + target type, and you must have EXECUTE privilege on the + formatter function. + + + + + Parameters + + + + source_type + + + The data type of the value passed to the formatter (the source of the + cast). + + + + + + target_type + + + The data type returned by the formatter (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 formatter 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 FORMATTER FOR CAST (integer AS text) + WITH FUNCTION int4_to_text_fmt(integer, text); + + + + + Compatibility + + + CREATE FORMATTER is a + PostgreSQL extension. + + + + + See Also + + + + + + + + diff --git a/doc/src/sgml/ref/drop_formatter.sgml b/doc/src/sgml/ref/drop_formatter.sgml new file mode 100644 index 00000000000..b7f367a590e --- /dev/null +++ b/doc/src/sgml/ref/drop_formatter.sgml @@ -0,0 +1,126 @@ + + + + + DROP FORMATTER + + + + DROP FORMATTER + 7 + SQL - Language Statements + + + + DROP FORMATTER + remove a formatter + + + + +DROP FORMATTER [ IF EXISTS ] FOR CAST (source_type AS target_type) [ CASCADE | RESTRICT ] + + + + + Description + + + DROP FORMATTER removes a previously defined formatter for + the given pair of source and target types. + + + + Dropping a formatter requires ownership of either the source type or the + target type. + + + + + Parameters + + + + IF EXISTS + + + Do not throw an error if the formatter does not exist. A notice is + issued in this case. + + + + + + source_type + + + The source data type of the formatter. + + + + + + target_type + + + The target data type of the formatter. + + + + + + CASCADE + + + Automatically drop objects that depend on the formatter, + and in turn all objects that depend on those objects + (see ). + + + + + + RESTRICT + + + Refuse to drop the formatter if any objects depend on it. This is the + default. + + + + + + + + Examples + + + To drop the formatter for the cast from integer to + text: + +DROP FORMATTER FOR CAST (integer AS text); + + + + + Compatibility + + + DROP FORMATTER 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..a1d055509c8 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -98,6 +98,7 @@ &createExtension; &createForeignDataWrapper; &createForeignTable; + &createFormatter; &createFunction; &createGroup; &createIndex; @@ -146,6 +147,7 @@ &dropExtension; &dropForeignDataWrapper; &dropForeignTable; + &dropFormatter; &dropFunction; &dropGroup; &dropIndex; diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..0f0211dd6c3 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -2794,6 +2794,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_AMPROC: case OBJECT_ATTRIBUTE: case OBJECT_CAST: + case OBJECT_FORMATTER: case OBJECT_DEFAULT: case OBJECT_DEFACL: case OBJECT_DOMCONSTRAINT: @@ -2937,6 +2938,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_AMPROC: case OBJECT_ATTRIBUTE: case OBJECT_CAST: + case OBJECT_FORMATTER: case OBJECT_DEFAULT: case OBJECT_DEFACL: case OBJECT_DOMCONSTRAINT: diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index c54774b3275..7eeac91264f 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -41,6 +41,7 @@ #include "catalog/pg_extension.h" #include "catalog/pg_foreign_data_wrapper.h" #include "catalog/pg_foreign_server.h" +#include "catalog/pg_formatter.h" #include "catalog/pg_init_privs.h" #include "catalog/pg_language.h" #include "catalog/pg_largeobject.h" @@ -1534,6 +1535,7 @@ doDeletion(const ObjectAddress *object, int flags) case DefaultAclRelationId: case EventTriggerRelationId: case TransformRelationId: + case FormatterRelationId: case AuthMemRelationId: DropObjectById(object); break; diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index af0e4703616..fcf38db8c55 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -37,6 +37,7 @@ #include "catalog/pg_extension.h" #include "catalog/pg_foreign_data_wrapper.h" #include "catalog/pg_foreign_server.h" +#include "catalog/pg_formatter.h" #include "catalog/pg_language.h" #include "catalog/pg_largeobject.h" #include "catalog/pg_largeobject_metadata.h" @@ -70,6 +71,7 @@ #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" +#include "commands/formatter.h" #include "commands/policy.h" #include "commands/proclang.h" #include "commands/tablespace.h" @@ -179,6 +181,20 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_CAST, false }, + { + "formatter", + FormatterRelationId, + FormatterOidIndexId, + FORMATTEROID, + SYSCACHEID_INVALID, + Anum_pg_formatter_oid, + InvalidAttrNumber, + InvalidAttrNumber, + InvalidAttrNumber, + InvalidAttrNumber, + OBJECT_FORMATTER, + false + }, { "collation", CollationRelationId, @@ -796,6 +812,9 @@ static const struct object_type_map { "cast", OBJECT_CAST }, + { + "formatter", OBJECT_FORMATTER + }, { "collation", OBJECT_COLLATION }, @@ -1165,6 +1184,21 @@ get_object_address(ObjectType objtype, Node *object, address.objectSubId = 0; } break; + case OBJECT_FORMATTER: + { + TypeName *sourcetype = linitial_node(TypeName, castNode(List, object)); + TypeName *targettype = lsecond_node(TypeName, castNode(List, object)); + Oid sourcetypeid; + Oid targettypeid; + + sourcetypeid = LookupTypeNameOid(NULL, sourcetype, missing_ok); + targettypeid = LookupTypeNameOid(NULL, targettype, missing_ok); + address.classId = FormatterRelationId; + address.objectId = + get_formatter_oid(sourcetypeid, targettypeid, missing_ok); + address.objectSubId = 0; + } + break; case OBJECT_TRANSFORM: { TypeName *typename = linitial_node(TypeName, castNode(List, object)); @@ -2239,6 +2273,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) * exceptions. */ if (type == OBJECT_TYPE || type == OBJECT_DOMAIN || type == OBJECT_CAST || + type == OBJECT_FORMATTER || type == OBJECT_TRANSFORM || type == OBJECT_DOMCONSTRAINT) { Datum *elems; @@ -2291,6 +2326,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) type == OBJECT_ROUTINE || type == OBJECT_OPERATOR || type == OBJECT_CAST || + type == OBJECT_FORMATTER || type == OBJECT_AMOP || type == OBJECT_AMPROC) { @@ -2336,6 +2372,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) pg_fallthrough; case OBJECT_DOMCONSTRAINT: case OBJECT_CAST: + case OBJECT_FORMATTER: case OBJECT_PUBLICATION_REL: case OBJECT_DEFACL: case OBJECT_TRANSFORM: @@ -2424,6 +2461,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) objnode = (Node *) typename; break; case OBJECT_CAST: + case OBJECT_FORMATTER: case OBJECT_DOMCONSTRAINT: case OBJECT_TRANSFORM: objnode = (Node *) list_make2(typename, linitial(args)); @@ -2583,6 +2621,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, address.objectId))); break; case OBJECT_CAST: + case OBJECT_FORMATTER: { /* We can only check permissions on the source/target types */ TypeName *sourcetype = linitial_node(TypeName, castNode(List, object)); @@ -3111,6 +3150,31 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case FormatterRelationId: + { + HeapTuple fmtTup; + Form_pg_formatter fmtForm; + + fmtTup = SearchSysCache1(FORMATTEROID, + ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(fmtTup)) + { + if (!missing_ok) + elog(ERROR, "could not find tuple for formatter %u", + object->objectId); + break; + } + + fmtForm = (Form_pg_formatter) GETSTRUCT(fmtTup); + + appendStringInfo(&buffer, _("formatter for cast from %s to %s"), + format_type_be(fmtForm->fmtsource), + format_type_be(fmtForm->fmttarget)); + + ReleaseSysCache(fmtTup); + break; + } + case CollationRelationId: { HeapTuple collTup; @@ -4762,6 +4826,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "cast"); break; + case FormatterRelationId: + appendStringInfoString(&buffer, "formatter"); + break; + case CollationRelationId: appendStringInfoString(&buffer, "collation"); break; @@ -5225,6 +5293,43 @@ getObjectIdentityParts(const ObjectAddress *object, break; } + case FormatterRelationId: + { + Relation fmtRel; + HeapTuple tup; + Form_pg_formatter fmtForm; + + fmtRel = table_open(FormatterRelationId, AccessShareLock); + + tup = get_catalog_object_by_oid(fmtRel, Anum_pg_formatter_oid, + object->objectId); + + if (!HeapTupleIsValid(tup)) + { + if (!missing_ok) + elog(ERROR, "could not find tuple for formatter %u", + object->objectId); + + table_close(fmtRel, AccessShareLock); + break; + } + + fmtForm = (Form_pg_formatter) GETSTRUCT(tup); + + appendStringInfo(&buffer, "for cast (%s AS %s)", + format_type_be_qualified(fmtForm->fmtsource), + format_type_be_qualified(fmtForm->fmttarget)); + + if (objname) + { + *objname = list_make1(format_type_be_qualified(fmtForm->fmtsource)); + *objargs = list_make1(format_type_be_qualified(fmtForm->fmttarget)); + } + + table_close(fmtRel, AccessShareLock); + break; + } + case CollationRelationId: { HeapTuple collTup; diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index 5b9d084977e..7a4066867e0 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -38,6 +38,7 @@ OBJS = \ explain_state.o \ extension.o \ foreigncmds.o \ + formattercmds.o \ functioncmds.o \ indexcmds.o \ lockcmds.o \ diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index 88a2df65c69..90e250f85ba 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -25,6 +25,7 @@ #include "miscadmin.h" #include "parser/parse_type.h" #include "utils/acl.h" +#include "utils/builtins.h" #include "utils/lsyscache.h" @@ -401,6 +402,27 @@ does_not_exist_skipping(ObjectType objtype, Node *object) } } break; + case OBJECT_FORMATTER: + { + if (!type_in_list_does_not_exist_skipping(list_make1(linitial(castNode(List, object))), &msg, &name) && + !type_in_list_does_not_exist_skipping(list_make1(lsecond(castNode(List, object))), &msg, &name)) + { + /* + * Both types exist (else the checks above would have + * produced a message), so resolve them to OIDs and report + * them with format_type_be(). This keeps the wording + * consistent with the duplicate-object and undefined-object + * errors, which also use format_type_be(). + */ + Oid sourcetypeid = typenameTypeId(NULL, linitial_node(TypeName, castNode(List, object))); + Oid targettypeid = typenameTypeId(NULL, lsecond_node(TypeName, castNode(List, object))); + + msg = gettext_noop("formatter for cast from type %s to type %s does not exist, skipping"); + name = format_type_be(sourcetypeid); + args = format_type_be(targettypeid); + } + } + break; case OBJECT_TRANSFORM: if (!type_in_list_does_not_exist_skipping(list_make1(linitial(castNode(List, object))), &msg, &name)) { diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index adc6eabc0f4..139bdf51c61 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2301,6 +2301,7 @@ stringify_grant_objtype(ObjectType objtype) case OBJECT_AMPROC: case OBJECT_ATTRIBUTE: case OBJECT_CAST: + case OBJECT_FORMATTER: case OBJECT_COLLATION: case OBJECT_CONVERSION: case OBJECT_DEFAULT: @@ -2385,6 +2386,7 @@ stringify_adefprivs_objtype(ObjectType objtype) case OBJECT_AMPROC: case OBJECT_ATTRIBUTE: case OBJECT_CAST: + case OBJECT_FORMATTER: case OBJECT_COLLATION: case OBJECT_CONVERSION: case OBJECT_DEFAULT: diff --git a/src/backend/commands/formattercmds.c b/src/backend/commands/formattercmds.c new file mode 100644 index 00000000000..2276519f562 --- /dev/null +++ b/src/backend/commands/formattercmds.c @@ -0,0 +1,245 @@ +/*------------------------------------------------------------------------- + * + * formattercmds.c + * Routines for SQL commands that manipulate formatters. + * + * A formatter associates a function with a (source type, target type) pair. + * The function implements CAST(expr AS target FORMAT format_expr): it + * receives the source value and the FORMAT expression (passed as text) and + * returns the target type. Formatters are registered in the pg_formatter + * catalog and are intentionally separate from pg_cast (see pg_formatter.h). + * + * This file provides the catalog registration machinery (CREATE/DROP + * FORMATTER). It does not perform expression transformation or execution of + * formatted casts. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/formattercmds.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/htup_details.h" +#include "access/table.h" +#include "catalog/catalog.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/objectaccess.h" +#include "catalog/objectaddress.h" +#include "catalog/pg_formatter.h" +#include "catalog/pg_proc.h" +#include "catalog/pg_type.h" +#include "commands/formatter.h" +#include "miscadmin.h" +#include "parser/parse_func.h" +#include "parser/parse_type.h" +#include "utils/acl.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/rel.h" +#include "utils/syscache.h" + +/* + * Validate the signature of a formatter function: it must be a normal + * function (not a procedure, aggregate or window function), must not return + * a set, and must have the signature + * + * formatter(source_type, text) returns target_type + * + * NB: the formatter signature is fixed at two arguments and does not include + * the target type modifier; typmod-aware formatter signatures are not + * supported. + */ +static void +check_formatter_function(Form_pg_proc procstruct, + Oid sourcetypeid, Oid targettypeid) +{ + if (procstruct->prokind != PROKIND_FUNCTION) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("formatter function must be a normal function"))); + if (procstruct->proretset) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("formatter function must not return a set"))); + if (procstruct->pronargs != 2) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("formatter function must take exactly two arguments"))); + if (procstruct->proargtypes.values[0] != sourcetypeid) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("first argument of formatter function must be type %s", + format_type_be(sourcetypeid)))); + if (procstruct->proargtypes.values[1] != TEXTOID) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("second argument of formatter function must be type %s", + "text"))); + if (procstruct->prorettype != targettypeid) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("return data type of formatter function must be type %s", + format_type_be(targettypeid)))); +} + +/* + * CREATE FORMATTER + */ +ObjectAddress +CreateFormatter(CreateFormatterStmt *stmt) +{ + Oid sourcetypeid; + Oid targettypeid; + char sourcetyptype; + char targettyptype; + Oid funcid; + AclResult aclresult; + Form_pg_proc procstruct; + HeapTuple tuple; + HeapTuple newtuple; + Datum values[Natts_pg_formatter]; + bool nulls[Natts_pg_formatter] = {0}; + Oid formatterid; + Relation relation; + ObjectAddress myself, + referenced; + ObjectAddresses *addrs; + + sourcetypeid = typenameTypeId(NULL, stmt->sourcetype); + targettypeid = typenameTypeId(NULL, stmt->targettype); + sourcetyptype = get_typtype(sourcetypeid); + targettyptype = get_typtype(targettypeid); + + /* No pseudo-types allowed */ + if (sourcetyptype == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("source data type %s is a pseudo-type", + TypeNameToString(stmt->sourcetype)))); + if (targettyptype == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("target data type %s is a pseudo-type", + TypeNameToString(stmt->targettype)))); + + /* + * Permission check. As for CREATE CAST, the caller must own at least one + * of the two types involved; owning a type is what authorizes defining + * conversion behavior for it. In addition, and following CREATE OPERATOR + * and CREATE AGGREGATE, we require EXECUTE permission on the formatter + * function (this will also be checked when the formatter is used, but it + * is a good idea to verify it up front). We intentionally do not require + * ownership of the function, unlike CREATE TRANSFORM, because a formatter + * is a data conversion rather than a procedural-language binding. + */ + if (!object_ownercheck(TypeRelationId, sourcetypeid, GetUserId()) + && !object_ownercheck(TypeRelationId, targettypeid, GetUserId())) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be owner of type %s or type %s", + format_type_be(sourcetypeid), + format_type_be(targettypeid)))); + + /* + * Look up and validate the formatter function. + */ + funcid = LookupFuncWithArgs(OBJECT_FUNCTION, stmt->func, false); + + aclresult = object_aclcheck(ProcedureRelationId, funcid, GetUserId(), ACL_EXECUTE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_FUNCTION, + NameListToString(stmt->func->objname)); + + tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for function %u", funcid); + procstruct = (Form_pg_proc) GETSTRUCT(tuple); + check_formatter_function(procstruct, sourcetypeid, targettypeid); + ReleaseSysCache(tuple); + + /* + * Check for a pre-existing formatter for this (source, target) pair. For + * this version only one formatter per pair is allowed. + */ + relation = table_open(FormatterRelationId, RowExclusiveLock); + + if (SearchSysCacheExists2(FORMATTERSOURCETARGET, + ObjectIdGetDatum(sourcetypeid), + ObjectIdGetDatum(targettypeid))) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("formatter for cast from type %s to type %s already exists", + format_type_be(sourcetypeid), + format_type_be(targettypeid)))); + + /* + * Build and insert the catalog tuple. + */ + formatterid = GetNewOidWithIndex(relation, FormatterOidIndexId, + Anum_pg_formatter_oid); + values[Anum_pg_formatter_oid - 1] = ObjectIdGetDatum(formatterid); + values[Anum_pg_formatter_fmtsource - 1] = ObjectIdGetDatum(sourcetypeid); + values[Anum_pg_formatter_fmttarget - 1] = ObjectIdGetDatum(targettypeid); + values[Anum_pg_formatter_fmtfunc - 1] = ObjectIdGetDatum(funcid); + + newtuple = heap_form_tuple(RelationGetDescr(relation), values, nulls); + CatalogTupleInsert(relation, newtuple); + + addrs = new_object_addresses(); + + ObjectAddressSet(myself, FormatterRelationId, formatterid); + + /* dependency on source type */ + ObjectAddressSet(referenced, TypeRelationId, sourcetypeid); + add_exact_object_address(&referenced, addrs); + + /* dependency on target type */ + ObjectAddressSet(referenced, TypeRelationId, targettypeid); + add_exact_object_address(&referenced, addrs); + + /* dependency on the formatter function */ + ObjectAddressSet(referenced, ProcedureRelationId, funcid); + add_exact_object_address(&referenced, addrs); + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + free_object_addresses(addrs); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + /* Post creation hook for new formatter */ + InvokeObjectPostCreateHook(FormatterRelationId, formatterid, 0); + + heap_freetuple(newtuple); + + table_close(relation, RowExclusiveLock); + + return myself; +} + +/* + * get_formatter_oid - given source and target type OIDs, look up a + * formatter OID + */ +Oid +get_formatter_oid(Oid sourcetypeid, Oid targettypeid, bool missing_ok) +{ + Oid oid; + + oid = GetSysCacheOid2(FORMATTERSOURCETARGET, Anum_pg_formatter_oid, + ObjectIdGetDatum(sourcetypeid), + ObjectIdGetDatum(targettypeid)); + if (!OidIsValid(oid) && !missing_ok) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("formatter for cast from type %s to type %s does not exist", + format_type_be(sourcetypeid), + format_type_be(targettypeid)))); + return oid; +} diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index 9f258d566eb..8c4ecb83c3a 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -26,6 +26,7 @@ backend_sources += files( 'explain_state.c', 'extension.c', 'foreigncmds.c', + 'formattercmds.c', 'functioncmds.c', 'indexcmds.c', 'lockcmds.c', diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index 77542d04200..f7d1779dad8 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -66,6 +66,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_AMPROC: case OBJECT_ATTRIBUTE: case OBJECT_CAST: + case OBJECT_FORMATTER: case OBJECT_COLLATION: case OBJECT_CONVERSION: case OBJECT_DEFAULT: diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index ef4881efc81..6e82a4e090e 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -295,13 +295,14 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt + CreateFormatterStmt CreatePropGraphStmt AlterPropGraphStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt DropOpClassStmt DropOpFamilyStmt DropStmt DropCastStmt DropRoleStmt DropdbStmt DropTableSpaceStmt - DropTransformStmt + DropTransformStmt DropFormatterStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt @@ -769,7 +770,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); EXPLAIN EXPRESSION EXTENSION EXTERNAL EXTRACT FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR - FORCE FOREIGN FORMAT FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS + FORCE FOREIGN FORMAT FORMATTER FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS GENERATED GLOBAL GRANT GRANTED GRAPH GRAPH_TABLE GREATEST GROUP_P GROUPING GROUPS @@ -1105,6 +1106,7 @@ stmt: | CreateStatsStmt | CreateTableSpaceStmt | CreateTransformStmt + | CreateFormatterStmt | CreateTrigStmt | CreateEventTrigStmt | CreateRoleStmt @@ -1125,6 +1127,7 @@ stmt: | DropSubscriptionStmt | DropTableSpaceStmt | DropTransformStmt + | DropFormatterStmt | DropRoleStmt | DropUserMappingStmt | DropdbStmt @@ -9893,6 +9896,38 @@ DropTransformStmt: DROP TRANSFORM opt_if_exists FOR Typename LANGUAGE name opt_d ; +/***************************************************************************** + * + * CREATE FORMATTER / DROP FORMATTER + * + * A formatter registers the function implementing + * CAST(expr AS target FORMAT format_expr) for a (source, target) type pair. + *****************************************************************************/ + +CreateFormatterStmt: CREATE FORMATTER FOR CAST '(' Typename AS Typename ')' WITH FUNCTION function_with_argtypes + { + CreateFormatterStmt *n = makeNode(CreateFormatterStmt); + + n->sourcetype = $6; + n->targettype = $8; + n->func = $12; + $$ = (Node *) n; + } + ; + +DropFormatterStmt: DROP FORMATTER opt_if_exists FOR CAST '(' Typename AS Typename ')' opt_drop_behavior + { + DropStmt *n = makeNode(DropStmt); + + n->removeType = OBJECT_FORMATTER; + n->objects = list_make1(list_make2($7, $9)); + n->behavior = $11; + n->missing_ok = $3; + $$ = (Node *) n; + } + ; + + /***************************************************************************** * * QUERY: @@ -18933,6 +18968,7 @@ unreserved_keyword: | FOLLOWING | FORCE | FORMAT + | FORMATTER | FORWARD | FUNCTION | FUNCTIONS diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..ee66296e0d0 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -37,6 +37,7 @@ #include "commands/event_trigger.h" #include "commands/explain.h" #include "commands/extension.h" +#include "commands/formatter.h" #include "commands/lockcmds.h" #include "commands/matview.h" #include "commands/policy.h" @@ -193,6 +194,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateTableAsStmt: case T_CreateTableSpaceStmt: case T_CreateTransformStmt: + case T_CreateFormatterStmt: case T_CreateTrigStmt: case T_CreateUserMappingStmt: case T_CreatedbStmt: @@ -1755,6 +1757,10 @@ ProcessUtilitySlow(ParseState *pstate, address = CreateTransform((CreateTransformStmt *) parsetree); break; + case T_CreateFormatterStmt: + address = CreateFormatter((CreateFormatterStmt *) parsetree); + break; + case T_AlterOpFamilyStmt: AlterOpFamily((AlterOpFamilyStmt *) parsetree); /* commands are stashed in AlterOpFamily */ @@ -2666,6 +2672,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_TRANSFORM: tag = CMDTAG_DROP_TRANSFORM; break; + case OBJECT_FORMATTER: + tag = CMDTAG_DROP_FORMATTER; + break; case OBJECT_ACCESS_METHOD: tag = CMDTAG_DROP_ACCESS_METHOD; break; @@ -2981,6 +2990,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_CREATE_TRANSFORM; break; + case T_CreateFormatterStmt: + tag = CMDTAG_CREATE_FORMATTER; + break; + case T_CreateTrigStmt: tag = CMDTAG_CREATE_TRIGGER; break; @@ -3690,6 +3703,10 @@ GetCommandLogLevel(Node *parsetree) lev = LOGSTMT_DDL; break; + case T_CreateFormatterStmt: + lev = LOGSTMT_DDL; + break; + case T_AlterOpFamilyStmt: lev = LOGSTMT_DDL; break; diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index dc98c5c5c09..d70753905eb 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -188,6 +188,9 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading transforms"); getTransforms(fout); + pg_log_info("reading formatters"); + getFormatters(fout); + pg_log_info("reading table inheritance information"); inhinfo = getInherits(fout, &numInherits); diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index c56437d6057..92883740743 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -300,6 +300,7 @@ static void dumpProcLang(Archive *fout, const ProcLangInfo *plang); static void dumpFunc(Archive *fout, const FuncInfo *finfo); static void dumpCast(Archive *fout, const CastInfo *cast); static void dumpTransform(Archive *fout, const TransformInfo *transform); +static void dumpFormatter(Archive *fout, const FormatterInfo *formatter); static void dumpOpr(Archive *fout, const OprInfo *oprinfo); static void dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo); static void dumpOpclass(Archive *fout, const OpclassInfo *opcinfo); @@ -9320,6 +9321,83 @@ getTransforms(Archive *fout) destroyPQExpBuffer(query); } +/* + * getFormatters + * get basic information about every formatter in the system + */ +void +getFormatters(Archive *fout) +{ + PGresult *res; + int ntups; + int i; + PQExpBuffer query; + FormatterInfo *formatterinfo; + int i_tableoid; + int i_oid; + int i_fmtsource; + int i_fmttarget; + int i_fmtfunc; + + /* Formatters were introduced in v19 */ + if (fout->remoteVersion < 190000) + return; + + query = createPQExpBuffer(); + + appendPQExpBufferStr(query, "SELECT tableoid, oid, " + "fmtsource, fmttarget, fmtfunc " + "FROM pg_formatter " + "ORDER BY 3,4"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + formatterinfo = pg_malloc_array(FormatterInfo, ntups); + + i_tableoid = PQfnumber(res, "tableoid"); + i_oid = PQfnumber(res, "oid"); + i_fmtsource = PQfnumber(res, "fmtsource"); + i_fmttarget = PQfnumber(res, "fmttarget"); + i_fmtfunc = PQfnumber(res, "fmtfunc"); + + for (i = 0; i < ntups; i++) + { + PQExpBufferData namebuf; + TypeInfo *sTypeInfo; + TypeInfo *tTypeInfo; + + formatterinfo[i].dobj.objType = DO_FORMATTER; + formatterinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid)); + formatterinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&formatterinfo[i].dobj); + formatterinfo[i].fmtsource = atooid(PQgetvalue(res, i, i_fmtsource)); + formatterinfo[i].fmttarget = atooid(PQgetvalue(res, i, i_fmttarget)); + formatterinfo[i].fmtfunc = atooid(PQgetvalue(res, i, i_fmtfunc)); + + /* + * Try to name the formatter as a concatenation of the type names. + * This is only used for purposes of sorting. If we fail to find + * either type, the name will be an empty string. + */ + initPQExpBuffer(&namebuf); + sTypeInfo = findTypeByOid(formatterinfo[i].fmtsource); + tTypeInfo = findTypeByOid(formatterinfo[i].fmttarget); + if (sTypeInfo && tTypeInfo) + appendPQExpBuffer(&namebuf, "%s %s", + sTypeInfo->dobj.name, tTypeInfo->dobj.name); + formatterinfo[i].dobj.name = namebuf.data; + + /* Decide whether we want to dump it */ + selectDumpableObject(&(formatterinfo[i].dobj), fout); + } + + PQclear(res); + + destroyPQExpBuffer(query); +} + /* * getTableAttrs - * for each interesting table, read info about its attributes @@ -11913,6 +11991,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_TRANSFORM: dumpTransform(fout, (const TransformInfo *) dobj); break; + case DO_FORMATTER: + dumpFormatter(fout, (const FormatterInfo *) dobj); + break; case DO_SEQUENCE_SET: dumpSequenceData(fout, (const TableDataInfo *) dobj); break; @@ -14255,6 +14336,90 @@ dumpTransform(Archive *fout, const TransformInfo *transform) destroyPQExpBuffer(transformargs); } +/* + * Dump a formatter + */ +static void +dumpFormatter(Archive *fout, const FormatterInfo *formatter) +{ + DumpOptions *dopt = fout->dopt; + PQExpBuffer defqry; + PQExpBuffer delqry; + PQExpBuffer labelq; + PQExpBuffer formatterargs; + FuncInfo *funcInfo; + const char *sourceType; + const char *targetType; + + /* Do nothing if not dumping schema */ + if (!dopt->dumpSchema) + return; + + /* Cannot dump if we don't have the formatter function's info */ + funcInfo = findFuncByOid(formatter->fmtfunc); + if (funcInfo == NULL) + pg_fatal("could not find function definition for function with OID %u", + formatter->fmtfunc); + + defqry = createPQExpBuffer(); + delqry = createPQExpBuffer(); + labelq = createPQExpBuffer(); + formatterargs = createPQExpBuffer(); + + sourceType = getFormattedTypeName(fout, formatter->fmtsource, zeroAsNone); + targetType = getFormattedTypeName(fout, formatter->fmttarget, zeroAsNone); + + appendPQExpBuffer(delqry, "DROP FORMATTER FOR CAST (%s AS %s);\n", + sourceType, targetType); + + appendPQExpBuffer(defqry, "CREATE FORMATTER FOR CAST (%s AS %s) ", + sourceType, targetType); + + { + char *fsig = format_function_signature(fout, funcInfo, true); + + /* + * Always qualify the function name (format_function_signature won't + * qualify it). + */ + appendPQExpBuffer(defqry, "WITH FUNCTION %s.%s", + fmtId(funcInfo->dobj.namespace->dobj.name), fsig); + free(fsig); + } + appendPQExpBufferStr(defqry, ";\n"); + + appendPQExpBuffer(labelq, "FORMATTER FOR CAST (%s AS %s)", + sourceType, targetType); + + appendPQExpBuffer(formatterargs, "FOR CAST (%s AS %s)", + sourceType, targetType); + + if (dopt->binary_upgrade) + binary_upgrade_extension_member(defqry, &formatter->dobj, + "FORMATTER", formatterargs->data, NULL); + + if (formatter->dobj.dump & DUMP_COMPONENT_DEFINITION) + ArchiveEntry(fout, formatter->dobj.catId, formatter->dobj.dumpId, + ARCHIVE_OPTS(.tag = labelq->data, + .description = "FORMATTER", + .section = SECTION_PRE_DATA, + .createStmt = defqry->data, + .dropStmt = delqry->data, + .deps = formatter->dobj.dependencies, + .nDeps = formatter->dobj.nDeps)); + + /* Dump Formatter Comments */ + if (formatter->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, "FORMATTER", formatterargs->data, + NULL, "", + formatter->dobj.catId, 0, formatter->dobj.dumpId); + + destroyPQExpBuffer(defqry); + destroyPQExpBuffer(delqry); + destroyPQExpBuffer(labelq); + destroyPQExpBuffer(formatterargs); +} + /* * dumpOpr @@ -20696,6 +20861,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_FDW: case DO_FOREIGN_SERVER: case DO_TRANSFORM: + case DO_FORMATTER: /* Pre-data objects: must come before the pre-data boundary */ addObjectDependency(preDataBound, dobj->dumpId); break; diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 5a6726d8b12..b3adb97b5ba 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -73,6 +73,7 @@ typedef enum DO_FOREIGN_SERVER, DO_DEFAULT_ACL, DO_TRANSFORM, + DO_FORMATTER, DO_LARGE_OBJECT, DO_LARGE_OBJECT_DATA, DO_PRE_DATA_BOUNDARY, @@ -559,6 +560,14 @@ typedef struct _transformInfo Oid trftosql; } TransformInfo; +typedef struct _formatterInfo +{ + DumpableObject dobj; + Oid fmtsource; + Oid fmttarget; + Oid fmtfunc; +} FormatterInfo; + /* InhInfo isn't a DumpableObject, just temporary state */ typedef struct _inhInfo { @@ -814,6 +823,7 @@ extern void getTriggers(Archive *fout, TableInfo tblinfo[], int numTables); extern void getProcLangs(Archive *fout); extern void getCasts(Archive *fout); extern void getTransforms(Archive *fout); +extern void getFormatters(Archive *fout); extern void getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables); extern bool shouldPrintColumn(const DumpOptions *dopt, const TableInfo *tbinfo, int colno); extern void getTSParsers(Archive *fout); diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index 03e5c1c1116..65df81c9324 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -60,6 +60,7 @@ enum dbObjectTypePriorities PRIO_EXTENSION, PRIO_TYPE, /* used for DO_TYPE and DO_SHELL_TYPE */ PRIO_CAST, + PRIO_FORMATTER, PRIO_FUNC, PRIO_AGG, PRIO_ACCESS_METHOD, @@ -128,6 +129,7 @@ static const int dbObjectTypePriority[] = [DO_FK_CONSTRAINT] = PRIO_FK_CONSTRAINT, [DO_PROCLANG] = PRIO_PROCLANG, [DO_CAST] = PRIO_CAST, + [DO_FORMATTER] = PRIO_FORMATTER, [DO_TABLE_DATA] = PRIO_TABLE_DATA, [DO_SEQUENCE_SET] = PRIO_SEQUENCE_SET, [DO_DUMMY_TYPE] = PRIO_DUMMY_TYPE, @@ -1649,6 +1651,13 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) ((CastInfo *) obj)->casttarget, obj->dumpId, obj->catId.oid); return; + case DO_FORMATTER: + snprintf(buf, bufsize, + "FORMATTER %u to %u (ID %d OID %u)", + ((FormatterInfo *) obj)->fmtsource, + ((FormatterInfo *) obj)->fmttarget, + obj->dumpId, obj->catId.oid); + return; case DO_TRANSFORM: snprintf(buf, bufsize, "TRANSFORM %u lang %u (ID %d OID %u)", diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index bab57372b88..872f22b3910 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -75,6 +75,7 @@ CATALOG_HEADERS := \ pg_parameter_acl.h \ pg_partitioned_table.h \ pg_range.h \ + pg_formatter.h \ pg_transform.h \ pg_sequence.h \ pg_publication.h \ diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 635c0d9cb13..6402cc76c87 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202606281 +#define CATALOG_VERSION_NO 202606282 #endif diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index fa836e4ee25..ea7fef5744d 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -62,6 +62,7 @@ catalog_headers = [ 'pg_parameter_acl.h', 'pg_partitioned_table.h', 'pg_range.h', + 'pg_formatter.h', 'pg_transform.h', 'pg_sequence.h', 'pg_publication.h', diff --git a/src/include/catalog/pg_formatter.h b/src/include/catalog/pg_formatter.h new file mode 100644 index 00000000000..e41a1de54ce --- /dev/null +++ b/src/include/catalog/pg_formatter.h @@ -0,0 +1,62 @@ +/*------------------------------------------------------------------------- + * + * pg_formatter.h + * definition of the "formatter" system catalog (pg_formatter) + * + * A formatter registers a function that implements + * CAST(expr AS target FORMAT format_expr) for a particular + * (source type, target type) pair. The formatter function receives the + * source value and the FORMAT expression (as text) and returns the target + * type. This is intentionally separate from pg_cast: ordinary casts have + * implicit/assignment/explicit contexts and binary-coercible/WITH INOUT/ + * WITHOUT FUNCTION methods, whereas a formatted cast is always explicit and + * always requires a function. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_formatter.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_FORMATTER_H +#define PG_FORMATTER_H + +#include "catalog/genbki.h" +#include "catalog/pg_formatter_d.h" /* IWYU pragma: export */ + +/* ---------------- + * pg_formatter definition. cpp turns this into + * typedef struct FormData_pg_formatter + * ---------------- + */ +BEGIN_CATALOG_STRUCT + +CATALOG(pg_formatter,8647,FormatterRelationId) +{ + Oid oid; /* oid */ + Oid fmtsource BKI_LOOKUP(pg_type); /* source type */ + Oid fmttarget BKI_LOOKUP(pg_type); /* target type */ + Oid fmtfunc BKI_LOOKUP(pg_proc); /* formatter function */ +} FormData_pg_formatter; + +END_CATALOG_STRUCT + +/* ---------------- + * Form_pg_formatter corresponds to a pointer to a tuple with + * the format of pg_formatter relation. + * ---------------- + */ +typedef FormData_pg_formatter *Form_pg_formatter; + +DECLARE_UNIQUE_INDEX_PKEY(pg_formatter_oid_index, 8648, FormatterOidIndexId, pg_formatter, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_formatter_source_target_index, 8649, FormatterSourceTargetIndexId, pg_formatter, btree(fmtsource oid_ops, fmttarget oid_ops)); + +MAKE_SYSCACHE(FORMATTEROID, pg_formatter_oid_index, 8); +MAKE_SYSCACHE(FORMATTERSOURCETARGET, pg_formatter_source_target_index, 8); + +#endif /* PG_FORMATTER_H */ diff --git a/src/include/commands/formatter.h b/src/include/commands/formatter.h new file mode 100644 index 00000000000..c68a8745670 --- /dev/null +++ b/src/include/commands/formatter.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * formatter.h + * prototypes for formattercmds.c. + * + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/formatter.h + * + *------------------------------------------------------------------------- + */ +#ifndef FORMATTER_H +#define FORMATTER_H + +#include "catalog/objectaddress.h" +#include "nodes/parsenodes.h" + +extern ObjectAddress CreateFormatter(CreateFormatterStmt *stmt); +extern Oid get_formatter_oid(Oid sourcetypeid, Oid targettypeid, + bool missing_ok); + +#endif /* FORMATTER_H */ diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 759c6bfae54..0b0431f4b6a 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2445,6 +2445,7 @@ typedef enum ObjectType OBJECT_FDW, OBJECT_FOREIGN_SERVER, OBJECT_FOREIGN_TABLE, + OBJECT_FORMATTER, OBJECT_FUNCTION, OBJECT_INDEX, OBJECT_LANGUAGE, @@ -4356,6 +4357,22 @@ typedef struct CreateTransformStmt ObjectWithArgs *tosql; } CreateTransformStmt; +/* ---------------------- + * CREATE FORMATTER Statement + * + * Registers a formatter function for CAST(... AS target FORMAT ...) on a + * (sourcetype, targettype) pair. DROP FORMATTER reuses the generic + * DropStmt path with removeType == OBJECT_FORMATTER. + * ---------------------- + */ +typedef struct CreateFormatterStmt +{ + NodeTag type; + TypeName *sourcetype; /* source data type */ + TypeName *targettype; /* target data type */ + ObjectWithArgs *func; /* formatter function */ +} CreateFormatterStmt; + /* ---------------------- * PREPARE Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 51ead54f015..7ce539c79f9 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -183,6 +183,7 @@ PG_KEYWORD("for", FOR, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("force", FORCE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("foreign", FOREIGN, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("format", FORMAT, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("formatter", FORMATTER, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("forward", FORWARD, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("freeze", FREEZE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("from", FROM, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index befae5f6b4f..c5701b4a8aa 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -95,6 +95,7 @@ PG_CMDTAG(CMDTAG_CREATE_EVENT_TRIGGER, "CREATE EVENT TRIGGER", false, false, fal PG_CMDTAG(CMDTAG_CREATE_EXTENSION, "CREATE EXTENSION", true, false, false) PG_CMDTAG(CMDTAG_CREATE_FOREIGN_DATA_WRAPPER, "CREATE FOREIGN DATA WRAPPER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_FOREIGN_TABLE, "CREATE FOREIGN TABLE", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_FORMATTER, "CREATE FORMATTER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_FUNCTION, "CREATE FUNCTION", true, false, false) PG_CMDTAG(CMDTAG_CREATE_INDEX, "CREATE INDEX", true, false, false) PG_CMDTAG(CMDTAG_CREATE_LANGUAGE, "CREATE LANGUAGE", true, false, false) @@ -148,6 +149,7 @@ PG_CMDTAG(CMDTAG_DROP_EVENT_TRIGGER, "DROP EVENT TRIGGER", false, false, false) PG_CMDTAG(CMDTAG_DROP_EXTENSION, "DROP EXTENSION", true, false, false) PG_CMDTAG(CMDTAG_DROP_FOREIGN_DATA_WRAPPER, "DROP FOREIGN DATA WRAPPER", true, false, false) PG_CMDTAG(CMDTAG_DROP_FOREIGN_TABLE, "DROP FOREIGN TABLE", true, false, false) +PG_CMDTAG(CMDTAG_DROP_FORMATTER, "DROP FORMATTER", true, false, false) PG_CMDTAG(CMDTAG_DROP_FUNCTION, "DROP FUNCTION", true, false, false) PG_CMDTAG(CMDTAG_DROP_INDEX, "DROP INDEX", true, false, false) PG_CMDTAG(CMDTAG_DROP_LANGUAGE, "DROP LANGUAGE", true, false, false) diff --git a/src/test/regress/expected/formatters.out b/src/test/regress/expected/formatters.out new file mode 100644 index 00000000000..bb1f091a1b8 --- /dev/null +++ b/src/test/regress/expected/formatters.out @@ -0,0 +1,116 @@ +-- +-- FORMATTERS +-- +-- CREATE/DROP FORMATTER registers formatter metadata in pg_formatter, +-- keyed by a (source type, target type) pair. This is catalog and DDL +-- infrastructure only; it does not transform or execute formatted casts. +-- A simple formatter function with the required signature +-- formatter(source_type, text) returns target_type +CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2; +CREATE FORMATTER FOR CAST (integer AS text) + WITH FUNCTION int4_to_text_fmt(integer, text); +-- It shows up in the catalog (use regtype/regprocedure, not raw OIDs) +SELECT fmtsource::regtype, fmttarget::regtype, fmtfunc::regprocedure + FROM pg_formatter + WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype; + fmtsource | fmttarget | fmtfunc +-----------+-----------+-------------------------------- + integer | text | int4_to_text_fmt(integer,text) +(1 row) + +-- ... and as a first-class object +SELECT pg_describe_object('pg_formatter'::regclass, oid, 0) + FROM pg_formatter + WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype; + pg_describe_object +----------------------------------------- + formatter for cast from integer to text +(1 row) + +-- Only one formatter per (source, target) pair +CREATE FORMATTER FOR CAST (integer AS text) + WITH FUNCTION int4_to_text_fmt(integer, text); +ERROR: formatter for cast from type integer to type text already exists +-- Function signature validation +CREATE FUNCTION fmt_bad_nargs(integer) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text; +CREATE FORMATTER FOR CAST (integer AS text) + WITH FUNCTION fmt_bad_nargs(integer); -- wrong # of arguments +ERROR: formatter function must take exactly two arguments +CREATE FUNCTION fmt_bad_arg2(integer, integer) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text; +CREATE FORMATTER FOR CAST (integer AS text) + WITH FUNCTION fmt_bad_arg2(integer, integer); -- second arg not text +ERROR: second argument of formatter function must be type text +CREATE FUNCTION fmt_bad_arg1(bigint, text) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text; +CREATE FORMATTER FOR CAST (integer AS text) + WITH FUNCTION fmt_bad_arg1(bigint, text); -- first arg mismatch +ERROR: first argument of formatter function must be type integer +CREATE FUNCTION fmt_bad_ret(integer, text) RETURNS boolean + LANGUAGE sql IMMUTABLE RETURN true; +CREATE FORMATTER FOR CAST (integer AS text) + WITH FUNCTION fmt_bad_ret(integer, text); -- return type mismatch +ERROR: return data type of formatter function must be type text +CREATE FUNCTION fmt_bad_set(integer, text) RETURNS SETOF text + LANGUAGE sql IMMUTABLE AS $$ SELECT $1::text $$; +CREATE FORMATTER FOR CAST (integer AS text) + WITH FUNCTION fmt_bad_set(integer, text); -- set-returning rejected +ERROR: formatter function must not return a set +-- No pseudo-types +CREATE FUNCTION fmt_anyel(anyelement, text) RETURNS text + LANGUAGE sql IMMUTABLE AS $$ SELECT $2 $$; +CREATE FORMATTER FOR CAST (anyelement AS text) + WITH FUNCTION fmt_anyel(anyelement, text); +ERROR: source data type anyelement is a pseudo-type +-- Registering a formatter does not enable a formatted cast: the FORMAT +-- clause must not be silently ignored or rewritten to a built-in function, +-- so CAST(... FORMAT ...) is rejected during parse analysis. +SELECT CAST(5 AS text FORMAT 'YYYY'); +ERROR: formatted casts are not implemented yet +LINE 1: SELECT CAST(5 AS text FORMAT 'YYYY'); + ^ +DETAIL: No formatter resolution mechanism is available. +-- Dependency behavior: the formatter depends on its function. +DROP FUNCTION int4_to_text_fmt(integer, text); -- fails (RESTRICT) +ERROR: cannot drop function int4_to_text_fmt(integer,text) because other objects depend on it +DETAIL: formatter for cast from integer to text depends on function int4_to_text_fmt(integer,text) +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP FUNCTION int4_to_text_fmt(integer, text) CASCADE; -- drops formatter +NOTICE: drop cascades to formatter for cast from integer to text +SELECT count(*) FROM pg_formatter + WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype; + count +------- + 0 +(1 row) + +-- Privileges: the creator must own the source or the target type. (The +-- ownership check happens before the function is looked up, so the function +-- name here is irrelevant.) +CREATE ROLE regress_formatter_user; +SET ROLE regress_formatter_user; +CREATE FORMATTER FOR CAST (text AS integer) + WITH FUNCTION pg_catalog.length(text); +ERROR: must be owner of type text or type integer +RESET ROLE; +DROP ROLE regress_formatter_user; +-- DROP FORMATTER, including IF EXISTS +CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2; +CREATE FORMATTER FOR CAST (integer AS text) + WITH FUNCTION int4_to_text_fmt(integer, text); +DROP FORMATTER FOR CAST (integer AS text); +DROP FORMATTER FOR CAST (integer AS text); -- fails, gone +ERROR: formatter for cast from type integer to type text does not exist +DROP FORMATTER IF EXISTS FOR CAST (integer AS text); -- notice, no error +NOTICE: formatter for cast from type integer to type text does not exist, skipping +-- Clean up +DROP FUNCTION int4_to_text_fmt(integer, text); +DROP FUNCTION fmt_bad_nargs(integer); +DROP FUNCTION fmt_bad_arg2(integer, integer); +DROP FUNCTION fmt_bad_arg1(bigint, text); +DROP FUNCTION fmt_bad_ret(integer, text); +DROP FUNCTION fmt_bad_set(integer, text); +DROP FUNCTION fmt_anyel(anyelement, text); diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index d64169b7bf0..6b60b02aab3 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -257,6 +257,9 @@ NOTICE: checking pg_range {rngmltconstruct1} => pg_proc {oid} NOTICE: checking pg_range {rngmltconstruct2} => pg_proc {oid} NOTICE: checking pg_range {rngcanonical} => pg_proc {oid} NOTICE: checking pg_range {rngsubdiff} => pg_proc {oid} +NOTICE: checking pg_formatter {fmtsource} => pg_type {oid} +NOTICE: checking pg_formatter {fmttarget} => pg_type {oid} +NOTICE: checking pg_formatter {fmtfunc} => pg_proc {oid} NOTICE: checking pg_transform {trftype} => pg_type {oid} NOTICE: checking pg_transform {trflang} => pg_language {oid} NOTICE: checking pg_transform {trffromsql} => pg_proc {oid} diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 8fa0a6c47fb..a0281f1fb2a 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi # ---------- # Another group of parallel tests # ---------- -test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse create_property_graph for_portion_of +test: create_aggregate create_function_sql create_cast formatters constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse create_property_graph for_portion_of # ---------- # sanity_check does a vacuum, affecting the sort order of SELECT * diff --git a/src/test/regress/sql/formatters.sql b/src/test/regress/sql/formatters.sql new file mode 100644 index 00000000000..8050aee6f45 --- /dev/null +++ b/src/test/regress/sql/formatters.sql @@ -0,0 +1,99 @@ +-- +-- FORMATTERS +-- +-- CREATE/DROP FORMATTER registers formatter metadata in pg_formatter, +-- keyed by a (source type, target type) pair. This is catalog and DDL +-- infrastructure only; it does not transform or execute formatted casts. + +-- A simple formatter function with the required signature +-- formatter(source_type, text) returns target_type +CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2; + +CREATE FORMATTER FOR CAST (integer AS text) + WITH FUNCTION int4_to_text_fmt(integer, text); + +-- It shows up in the catalog (use regtype/regprocedure, not raw OIDs) +SELECT fmtsource::regtype, fmttarget::regtype, fmtfunc::regprocedure + FROM pg_formatter + WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype; + +-- ... and as a first-class object +SELECT pg_describe_object('pg_formatter'::regclass, oid, 0) + FROM pg_formatter + WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype; + +-- Only one formatter per (source, target) pair +CREATE FORMATTER FOR CAST (integer AS text) + WITH FUNCTION int4_to_text_fmt(integer, text); + +-- Function signature validation +CREATE FUNCTION fmt_bad_nargs(integer) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text; +CREATE FORMATTER FOR CAST (integer AS text) + WITH FUNCTION fmt_bad_nargs(integer); -- wrong # of arguments + +CREATE FUNCTION fmt_bad_arg2(integer, integer) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text; +CREATE FORMATTER FOR CAST (integer AS text) + WITH FUNCTION fmt_bad_arg2(integer, integer); -- second arg not text + +CREATE FUNCTION fmt_bad_arg1(bigint, text) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text; +CREATE FORMATTER FOR CAST (integer AS text) + WITH FUNCTION fmt_bad_arg1(bigint, text); -- first arg mismatch + +CREATE FUNCTION fmt_bad_ret(integer, text) RETURNS boolean + LANGUAGE sql IMMUTABLE RETURN true; +CREATE FORMATTER FOR CAST (integer AS text) + WITH FUNCTION fmt_bad_ret(integer, text); -- return type mismatch + +CREATE FUNCTION fmt_bad_set(integer, text) RETURNS SETOF text + LANGUAGE sql IMMUTABLE AS $$ SELECT $1::text $$; +CREATE FORMATTER FOR CAST (integer AS text) + WITH FUNCTION fmt_bad_set(integer, text); -- set-returning rejected + +-- No pseudo-types +CREATE FUNCTION fmt_anyel(anyelement, text) RETURNS text + LANGUAGE sql IMMUTABLE AS $$ SELECT $2 $$; +CREATE FORMATTER FOR CAST (anyelement AS text) + WITH FUNCTION fmt_anyel(anyelement, text); + +-- Registering a formatter does not enable a formatted cast: the FORMAT +-- clause must not be silently ignored or rewritten to a built-in function, +-- so CAST(... FORMAT ...) is rejected during parse analysis. +SELECT CAST(5 AS text FORMAT 'YYYY'); + +-- Dependency behavior: the formatter depends on its function. +DROP FUNCTION int4_to_text_fmt(integer, text); -- fails (RESTRICT) +DROP FUNCTION int4_to_text_fmt(integer, text) CASCADE; -- drops formatter +SELECT count(*) FROM pg_formatter + WHERE fmtsource = 'integer'::regtype AND fmttarget = 'text'::regtype; + +-- Privileges: the creator must own the source or the target type. (The +-- ownership check happens before the function is looked up, so the function +-- name here is irrelevant.) +CREATE ROLE regress_formatter_user; +SET ROLE regress_formatter_user; +CREATE FORMATTER FOR CAST (text AS integer) + WITH FUNCTION pg_catalog.length(text); +RESET ROLE; +DROP ROLE regress_formatter_user; + +-- DROP FORMATTER, including IF EXISTS +CREATE FUNCTION int4_to_text_fmt(integer, text) RETURNS text + LANGUAGE sql IMMUTABLE RETURN $1::text || ':' || $2; +CREATE FORMATTER FOR CAST (integer AS text) + WITH FUNCTION int4_to_text_fmt(integer, text); +DROP FORMATTER FOR CAST (integer AS text); +DROP FORMATTER FOR CAST (integer AS text); -- fails, gone +DROP FORMATTER IF EXISTS FOR CAST (integer AS text); -- notice, no error + +-- Clean up +DROP FUNCTION int4_to_text_fmt(integer, text); +DROP FUNCTION fmt_bad_nargs(integer); +DROP FUNCTION fmt_bad_arg2(integer, integer); +DROP FUNCTION fmt_bad_arg1(bigint, text); +DROP FUNCTION fmt_bad_ret(integer, text); +DROP FUNCTION fmt_bad_set(integer, text); +DROP FUNCTION fmt_anyel(anyelement, text); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index c5db6ca6705..2feea74b3a5 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -575,6 +575,7 @@ CreateExtensionStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt +CreateFormatterStmt CreateFunctionStmt CreateOpClassItem CreateOpClassStmt @@ -921,6 +922,7 @@ FormData_pg_extension FormData_pg_foreign_data_wrapper FormData_pg_foreign_server FormData_pg_foreign_table +FormData_pg_formatter FormData_pg_index FormData_pg_inherits FormData_pg_language @@ -986,6 +988,7 @@ Form_pg_extension Form_pg_foreign_data_wrapper Form_pg_foreign_server Form_pg_foreign_table +Form_pg_formatter Form_pg_index Form_pg_inherits Form_pg_language @@ -1028,6 +1031,7 @@ Form_pg_ts_template Form_pg_type Form_pg_user_mapping FormatNode +FormatterInfo FreeBlockNumberArray FreeListData FreePageBtree -- 2.54.0