From 81cb0629f40b7e88317d041f12431054aca1cd91 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Thu, 6 Aug 2026 11:29:47 +0900 Subject: [PATCH v3] Distinguish publication exclusions in object addresses pg_publication_rel entries can represent either explicitly published relations or tables excluded from FOR ALL TABLES publications. However, object address code treated all pg_publication_rel entries as publication relations, without checking prexcept. That made EXCEPT entries look like ordinary published-table mappings in dependency messages, pg_identify_object(), and pg_identify_object_as_address(). Fix this by distinguishing EXCEPT entries in the object address code. They are now reported as publication excluded relation, with object identities indicating that the table is excluded from the publication. Also accept "publication excluded relation" as an object type in pg_get_object_address(), so the output of pg_identify_object_as_address() continues to round-trip. "publication relation" resolves only non-EXCEPT entries, while "publication excluded relation" resolves only EXCEPT entries. --- src/backend/catalog/aclchk.c | 2 + src/backend/catalog/objectaddress.c | 125 ++++++++++++++++--- src/backend/commands/dropcmds.c | 1 + src/backend/commands/event_trigger.c | 2 + src/backend/commands/seclabel.c | 1 + src/include/nodes/parsenodes.h | 1 + src/test/regress/expected/object_address.out | 26 +++- src/test/regress/expected/publication.out | 35 ++++++ src/test/regress/sql/object_address.sql | 20 ++- src/test/regress/sql/publication.sql | 29 +++++ 10 files changed, 223 insertions(+), 19 deletions(-) diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index e67358f3858..2bbcc3e824b 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -2765,6 +2765,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_DEFAULT: case OBJECT_DEFACL: case OBJECT_DOMCONSTRAINT: + case OBJECT_PUBLICATION_EXCLUDED_REL: case OBJECT_PUBLICATION_NAMESPACE: case OBJECT_PUBLICATION_REL: case OBJECT_ROLE: @@ -2906,6 +2907,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_DEFACL: case OBJECT_DOMCONSTRAINT: case OBJECT_PARAMETER_ACL: + case OBJECT_PUBLICATION_EXCLUDED_REL: case OBJECT_PUBLICATION_NAMESPACE: case OBJECT_PUBLICATION_REL: case OBJECT_ROLE: diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index b08e076e65f..e43f7360395 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -823,6 +823,9 @@ static const struct object_type_map { "publication relation", OBJECT_PUBLICATION_REL }, + { + "publication excluded relation", OBJECT_PUBLICATION_EXCLUDED_REL + }, { "subscription", OBJECT_SUBSCRIPTION }, @@ -863,7 +866,8 @@ static ObjectAddress get_object_address_opf_member(ObjectType objtype, static ObjectAddress get_object_address_usermapping(List *object, bool missing_ok); -static ObjectAddress get_object_address_publication_rel(List *object, +static ObjectAddress get_object_address_publication_rel(ObjectType objtype, + List *object, Relation *relp, bool missing_ok); static ObjectAddress get_object_address_publication_schema(List *object, @@ -880,6 +884,9 @@ static void getRelationTypeDescription(StringInfo buffer, Oid relid, int32 objectSubId, bool missing_ok); static void getProcedureTypeDescription(StringInfo buffer, Oid procid, bool missing_ok); +static void getPublicationRelationTypeDescription(StringInfo buffer, + Oid pubreloid, + bool missing_ok); static void getConstraintTypeDescription(StringInfo buffer, Oid constroid, bool missing_ok); static void getOpFamilyIdentity(StringInfo buffer, Oid opfid, List **object, @@ -1112,8 +1119,10 @@ get_object_address(ObjectType objtype, Node *object, address = get_object_address_publication_schema(castNode(List, object), missing_ok); break; + case OBJECT_PUBLICATION_EXCLUDED_REL: case OBJECT_PUBLICATION_REL: - address = get_object_address_publication_rel(castNode(List, object), + address = get_object_address_publication_rel(objtype, + castNode(List, object), &relation, missing_ok); break; @@ -1860,12 +1869,12 @@ get_object_address_usermapping(List *object, bool missing_ok) } /* - * Find the ObjectAddress for a publication relation. The first element of - * the object parameter is the relation name, the second is the - * publication name. + * Find the ObjectAddress for a published or excluded publication relation. + * The first element of the object parameter is the relation name, the second + * is the publication name. */ static ObjectAddress -get_object_address_publication_rel(List *object, +get_object_address_publication_rel(ObjectType objtype, List *object, Relation *relp, bool missing_ok) { ObjectAddress address; @@ -1873,6 +1882,9 @@ get_object_address_publication_rel(List *object, List *relname; char *pubname; Publication *pub; + Form_pg_publication_rel prform; + HeapTuple tup; + bool isexcept; ObjectAddressSet(address, PublicationRelRelationId, InvalidOid); @@ -1893,12 +1905,15 @@ get_object_address_publication_rel(List *object, return address; } - /* Find the publication relation mapping in syscache. */ - address.objectId = - GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid, - ObjectIdGetDatum(RelationGetRelid(relation)), - ObjectIdGetDatum(pub->oid)); - if (!OidIsValid(address.objectId)) + /* + * Find the publication relation mapping in syscache. Fetch the tuple + * rather than just its OID, so that prexcept can be checked without a + * second lookup. + */ + tup = SearchSysCache2(PUBLICATIONRELMAP, + ObjectIdGetDatum(RelationGetRelid(relation)), + ObjectIdGetDatum(pub->oid)); + if (!HeapTupleIsValid(tup)) { if (!missing_ok) ereport(ERROR, @@ -1909,6 +1924,30 @@ get_object_address_publication_rel(List *object, return address; } + prform = (Form_pg_publication_rel) GETSTRUCT(tup); + address.objectId = prform->oid; + isexcept = prform->prexcept; + ReleaseSysCache(tup); + + /* + * The same relation and publication pair identifies either a published or + * an excluded relation, so reject an entry of the kind that was not asked + * for. + */ + if (isexcept != (objtype == OBJECT_PUBLICATION_EXCLUDED_REL)) + { + if (isexcept) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a published relation of publication \"%s\"", + RelationGetRelationName(relation), pubname))); + else + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not an excluded relation of publication \"%s\"", + RelationGetRelationName(relation), pubname))); + } + *relp = relation; return address; } @@ -2235,6 +2274,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) pg_fallthrough; case OBJECT_DOMCONSTRAINT: case OBJECT_CAST: + case OBJECT_PUBLICATION_EXCLUDED_REL: case OBJECT_PUBLICATION_REL: case OBJECT_DEFACL: case OBJECT_TRANSFORM: @@ -2326,6 +2366,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_TRANSFORM: objnode = (Node *) list_make2(typename, linitial(args)); break; + case OBJECT_PUBLICATION_EXCLUDED_REL: case OBJECT_PUBLICATION_REL: objnode = (Node *) list_make2(name, linitial(args)); break; @@ -2554,6 +2595,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_AMPROC: case OBJECT_DEFAULT: case OBJECT_DEFACL: + case OBJECT_PUBLICATION_EXCLUDED_REL: case OBJECT_PUBLICATION_NAMESPACE: case OBJECT_PUBLICATION_REL: case OBJECT_USER_MAPPING: @@ -4020,9 +4062,18 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) initStringInfo(&rel); getRelationDescription(&rel, prform->prrelid, false); - /* translator: first %s is, e.g., "table %s" */ - appendStringInfo(&buffer, _("publication of %s in publication %s"), - rel.data, pubname); + if (prform->prexcept) + { + /* translator: first %s is, e.g., "table %s" */ + appendStringInfo(&buffer, _("exclusion of %s from publication %s"), + rel.data, pubname); + } + else + { + /* translator: first %s is, e.g., "table %s" */ + appendStringInfo(&buffer, _("publication of %s in publication %s"), + rel.data, pubname); + } pfree(rel.data); ReleaseSysCache(tup); break; @@ -4654,7 +4705,8 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) break; case PublicationRelRelationId: - appendStringInfoString(&buffer, "publication relation"); + getPublicationRelationTypeDescription(&buffer, object->objectId, + missing_ok); break; case SubscriptionRelationId: @@ -4808,6 +4860,41 @@ getProcedureTypeDescription(StringInfo buffer, Oid procid, ReleaseSysCache(procTup); } +/* + * subroutine for getObjectTypeDescription: describe a publication relation + * + * Appends "publication excluded relation" for EXCEPT entries, or + * "publication relation" for published relations, to buffer. + */ +static void +getPublicationRelationTypeDescription(StringInfo buffer, Oid pubreloid, + bool missing_ok) +{ + HeapTuple tup; + Form_pg_publication_rel prform; + + tup = SearchSysCache1(PUBLICATIONREL, ObjectIdGetDatum(pubreloid)); + if (!HeapTupleIsValid(tup)) + { + if (!missing_ok) + elog(ERROR, "cache lookup failed for publication table %u", + pubreloid); + + /* fallback to "publication relation" for an undefined object */ + appendStringInfoString(buffer, "publication relation"); + return; + } + + prform = (Form_pg_publication_rel) GETSTRUCT(tup); + + if (prform->prexcept) + appendStringInfoString(buffer, "publication excluded relation"); + else + appendStringInfoString(buffer, "publication relation"); + + ReleaseSysCache(tup); +} + /* * Obtain a given object's identity, as a palloc'ed string. * @@ -5949,7 +6036,11 @@ getObjectIdentityParts(const ObjectAddress *object, pubname = get_publication_name(prform->prpubid, false); getRelationIdentity(&buffer, prform->prrelid, objname, false); - appendStringInfo(&buffer, " in publication %s", pubname); + if (prform->prexcept) + appendStringInfo(&buffer, " excluded from publication %s", + pubname); + else + appendStringInfo(&buffer, " in publication %s", pubname); if (objargs) *objargs = list_make1(pubname); diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index 92526012d2a..228959e1050 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -504,6 +504,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object) case OBJECT_DOMCONSTRAINT: case OBJECT_LARGEOBJECT: case OBJECT_PARAMETER_ACL: + case OBJECT_PUBLICATION_EXCLUDED_REL: case OBJECT_PUBLICATION_NAMESPACE: case OBJECT_PUBLICATION_REL: case OBJECT_TABCONSTRAINT: diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index d868c7f42c3..08f8f4c3f24 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2316,6 +2316,7 @@ stringify_grant_objtype(ObjectType objtype) case OBJECT_OPFAMILY: case OBJECT_POLICY: case OBJECT_PUBLICATION: + case OBJECT_PUBLICATION_EXCLUDED_REL: case OBJECT_PUBLICATION_NAMESPACE: case OBJECT_PUBLICATION_REL: case OBJECT_ROLE: @@ -2400,6 +2401,7 @@ stringify_adefprivs_objtype(ObjectType objtype) case OBJECT_PARAMETER_ACL: case OBJECT_POLICY: case OBJECT_PUBLICATION: + case OBJECT_PUBLICATION_EXCLUDED_REL: case OBJECT_PUBLICATION_NAMESPACE: case OBJECT_PUBLICATION_REL: case OBJECT_ROLE: diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index 5b80396723c..77e3640e626 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -80,6 +80,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_OPFAMILY: case OBJECT_PARAMETER_ACL: case OBJECT_POLICY: + case OBJECT_PUBLICATION_EXCLUDED_REL: case OBJECT_PUBLICATION_NAMESPACE: case OBJECT_PUBLICATION_REL: case OBJECT_RULE: diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index aea7311faad..0debcd193ab 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2395,6 +2395,7 @@ typedef enum ObjectType OBJECT_POLICY, OBJECT_PROCEDURE, OBJECT_PUBLICATION, + OBJECT_PUBLICATION_EXCLUDED_REL, OBJECT_PUBLICATION_NAMESPACE, OBJECT_PUBLICATION_REL, OBJECT_ROLE, diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out index 4c266d7fa67..ae8844d7c52 100644 --- a/src/test/regress/expected/object_address.out +++ b/src/test/regress/expected/object_address.out @@ -111,7 +111,8 @@ BEGIN ('text search template'), ('text search configuration'), ('policy'), ('user mapping'), ('default acl'), ('transform'), ('operator of access method'), ('function of access method'), - ('publication namespace'), ('publication relation') + ('publication namespace'), ('publication relation'), + ('publication excluded relation') LOOP FOR names IN VALUES ('{eins}'), ('{addr_nsp, zwei}'), ('{eins, zwei, drei}') LOOP @@ -331,6 +332,12 @@ WARNING: error for publication relation,{addr_nsp,zwei},{}: argument list lengt WARNING: error for publication relation,{addr_nsp,zwei},{integer}: relation "addr_nsp.zwei" does not exist WARNING: error for publication relation,{eins,zwei,drei},{}: argument list length must be exactly 1 WARNING: error for publication relation,{eins,zwei,drei},{integer}: cross-database references are not implemented: "eins.zwei.drei" +WARNING: error for publication excluded relation,{eins},{}: argument list length must be exactly 1 +WARNING: error for publication excluded relation,{eins},{integer}: relation "eins" does not exist +WARNING: error for publication excluded relation,{addr_nsp,zwei},{}: argument list length must be exactly 1 +WARNING: error for publication excluded relation,{addr_nsp,zwei},{integer}: relation "addr_nsp.zwei" does not exist +WARNING: error for publication excluded relation,{eins,zwei,drei},{}: argument list length must be exactly 1 +WARNING: error for publication excluded relation,{eins,zwei,drei},{integer}: cross-database references are not implemented: "eins.zwei.drei" -- these object types cannot be qualified names SELECT pg_get_object_address('language', '{one}', '{}'); ERROR: language "one" does not exist @@ -509,6 +516,23 @@ subscription|NULL|regress_addr_sub|regress_addr_sub|t publication|NULL|addr_pub|addr_pub|t publication relation|NULL|NULL|addr_nsp.gentable in publication addr_pub|t publication namespace|NULL|NULL|addr_nsp in publication addr_pub_schema|t +-- A FOR ALL TABLES publication is listed by \d for every table in the +-- database, which would disturb the other tests running concurrently in this +-- parallel group. Create it in a transaction that is rolled back, so that it +-- is never visible to another session. +BEGIN; +SET LOCAL client_min_messages = 'ERROR'; +CREATE PUBLICATION addr_pub_except FOR ALL TABLES EXCEPT (TABLE addr_nsp.gentable); +SELECT (pg_identify_object(addr1.classid, addr1.objid, addr1.objsubid)).*, + ROW(pg_identify_object(addr1.classid, addr1.objid, addr1.objsubid)) = + ROW(pg_identify_object(addr2.classid, addr2.objid, addr2.objsubid)) AS roundtrip +FROM pg_get_object_address('publication excluded relation', + '{addr_nsp, gentable}', + '{addr_pub_except}') AS addr1, + pg_identify_object_as_address(classid, objid, objsubid) AS ioa (typ, nms, args), + pg_get_object_address(typ, nms, ioa.args) AS addr2; +publication excluded relation|NULL|NULL|addr_nsp.gentable excluded from publication addr_pub_except|t +ROLLBACK; --- --- Cleanup resources --- diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out index fe602c4538e..7ccfb5810a6 100644 --- a/src/test/regress/expected/publication.out +++ b/src/test/regress/expected/publication.out @@ -274,6 +274,41 @@ Excluded from publications: "testpub_foralltables_excepttable" "testpub_foralltables_excepttable1" +-- Check object address handling for an EXCEPT entry. +\a\t +SELECT (pg_identify_object('pg_publication_rel'::regclass, pr.oid, 0)).* +FROM pg_publication_rel pr +JOIN pg_publication p ON p.oid = pr.prpubid +JOIN pg_class c ON c.oid = pr.prrelid +WHERE p.pubname = 'testpub_foralltables_excepttable1' + AND c.relname = 'testpub_tbl1'; +publication excluded relation|||public.testpub_tbl1 excluded from publication testpub_foralltables_excepttable1 +-- testpub_describe publishes testpub_tbl1, testpub_foralltables_excepttable1 +-- excludes it; an entry of one kind must not be resolved as the other. +CREATE PUBLICATION testpub_describe FOR TABLE testpub_tbl1; +SELECT pg_get_object_address('publication excluded relation', + '{public, testpub_tbl1}', '{testpub_describe}'); +ERROR: "testpub_tbl1" is not an excluded relation of publication "testpub_describe" +SELECT pg_get_object_address('publication relation', + '{public, testpub_tbl1}', + '{testpub_foralltables_excepttable1}'); +ERROR: "testpub_tbl1" is not a published relation of publication "testpub_foralltables_excepttable1" +-- No entry of either kind. testpub_default publishes nothing. +SELECT pg_get_object_address('publication excluded relation', + '{public, testpub_tbl1}', '{testpub_default}'); +ERROR: publication relation "testpub_tbl1" in publication "testpub_default" does not exist +-- Check pg_describe_object output for both included and excluded entries +SELECT p.pubname, + pg_describe_object('pg_publication_rel'::regclass, pr.oid, 0) AS description, + pr.prexcept +FROM pg_publication_rel pr +JOIN pg_publication p ON p.oid = pr.prpubid +WHERE p.pubname IN ('testpub_describe', 'testpub_foralltables_excepttable1') +ORDER BY p.pubname; +testpub_describe|publication of table testpub_tbl1 in publication testpub_describe|f +testpub_foralltables_excepttable1|exclusion of table testpub_tbl1 from publication testpub_foralltables_excepttable1|t +DROP PUBLICATION testpub_describe; +\a\t -- fail - first table in the EXCEPT list should use TABLE keyword CREATE PUBLICATION testpub_foralltables_excepttable2 FOR ALL TABLES EXCEPT (testpub_tbl1, testpub_tbl2); ERROR: syntax error at or near "testpub_tbl1" diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql index 653a53038e3..b1cf02b8725 100644 --- a/src/test/regress/sql/object_address.sql +++ b/src/test/regress/sql/object_address.sql @@ -103,7 +103,8 @@ BEGIN ('text search template'), ('text search configuration'), ('policy'), ('user mapping'), ('default acl'), ('transform'), ('operator of access method'), ('function of access method'), - ('publication namespace'), ('publication relation') + ('publication namespace'), ('publication relation'), + ('publication excluded relation') LOOP FOR names IN VALUES ('{eins}'), ('{addr_nsp, zwei}'), ('{eins, zwei, drei}') LOOP @@ -225,6 +226,23 @@ FROM objects, pg_get_object_address(typ, nms, ioa.args) AS addr2 ORDER BY addr1.classid, addr1.objid, addr1.objsubid; +-- A FOR ALL TABLES publication is listed by \d for every table in the +-- database, which would disturb the other tests running concurrently in this +-- parallel group. Create it in a transaction that is rolled back, so that it +-- is never visible to another session. +BEGIN; +SET LOCAL client_min_messages = 'ERROR'; +CREATE PUBLICATION addr_pub_except FOR ALL TABLES EXCEPT (TABLE addr_nsp.gentable); +SELECT (pg_identify_object(addr1.classid, addr1.objid, addr1.objsubid)).*, + ROW(pg_identify_object(addr1.classid, addr1.objid, addr1.objsubid)) = + ROW(pg_identify_object(addr2.classid, addr2.objid, addr2.objsubid)) AS roundtrip +FROM pg_get_object_address('publication excluded relation', + '{addr_nsp, gentable}', + '{addr_pub_except}') AS addr1, + pg_identify_object_as_address(classid, objid, objsubid) AS ioa (typ, nms, args), + pg_get_object_address(typ, nms, ioa.args) AS addr2; +ROLLBACK; + --- --- Cleanup resources --- diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql index 80c244c9138..074482605d9 100644 --- a/src/test/regress/sql/publication.sql +++ b/src/test/regress/sql/publication.sql @@ -123,6 +123,35 @@ CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (TABL -- Check that the table description shows the publications where it is listed -- in the EXCEPT clause \d testpub_tbl1 +-- Check object address handling for an EXCEPT entry. +\a\t +SELECT (pg_identify_object('pg_publication_rel'::regclass, pr.oid, 0)).* +FROM pg_publication_rel pr +JOIN pg_publication p ON p.oid = pr.prpubid +JOIN pg_class c ON c.oid = pr.prrelid +WHERE p.pubname = 'testpub_foralltables_excepttable1' + AND c.relname = 'testpub_tbl1'; +-- testpub_describe publishes testpub_tbl1, testpub_foralltables_excepttable1 +-- excludes it; an entry of one kind must not be resolved as the other. +CREATE PUBLICATION testpub_describe FOR TABLE testpub_tbl1; +SELECT pg_get_object_address('publication excluded relation', + '{public, testpub_tbl1}', '{testpub_describe}'); +SELECT pg_get_object_address('publication relation', + '{public, testpub_tbl1}', + '{testpub_foralltables_excepttable1}'); +-- No entry of either kind. testpub_default publishes nothing. +SELECT pg_get_object_address('publication excluded relation', + '{public, testpub_tbl1}', '{testpub_default}'); +-- Check pg_describe_object output for both included and excluded entries +SELECT p.pubname, + pg_describe_object('pg_publication_rel'::regclass, pr.oid, 0) AS description, + pr.prexcept +FROM pg_publication_rel pr +JOIN pg_publication p ON p.oid = pr.prpubid +WHERE p.pubname IN ('testpub_describe', 'testpub_foralltables_excepttable1') +ORDER BY p.pubname; +DROP PUBLICATION testpub_describe; +\a\t -- fail - first table in the EXCEPT list should use TABLE keyword CREATE PUBLICATION testpub_foralltables_excepttable2 FOR ALL TABLES EXCEPT (testpub_tbl1, testpub_tbl2); -- 2.55.0