mirror of
https://github.com/postgres/postgres.git
synced 2026-03-22 02:20:53 -04:00
Allow table exclusions in publications via EXCEPT TABLE.
Extend CREATE PUBLICATION ... FOR ALL TABLES to support the EXCEPT TABLE syntax. This allows one or more tables to be excluded. The publisher will not send the data of excluded tables to the subscriber. To support this, pg_publication_rel now includes a prexcept column to flag excluded relations. For partitioned tables, the exclusion is applied at the root level; specifying a root table excludes all current and future partitions in that tree. Follow-up work will implement ALTER PUBLICATION support for managing these exclusions. Author: vignesh C <vignesh21@gmail.com> Author: Shlok Kyal <shlok.kyal.oss@gmail.com> Reviewed-by: shveta malik <shveta.malik@gmail.com> Reviewed-by: Amit Kapila <amit.kapila16@gmail.com> Reviewed-by: Peter Smith <smithpb2250@gmail.com> Reviewed-by: Dilip Kumar <dilipbalaut@gmail.com> Reviewed-by: Zhijie Hou <houzj.fnst@fujitsu.com> Reviewed-by: Nisha Moond <nisha.moond412@gmail.com> Reviewed-by: David G. Johnston <david.g.johnston@gmail.com> Reviewed-by: Ashutosh Sharma <ashu.coek88@gmail.com> Reviewed-by: Chao Li <li.evan.chao@gmail.com> Reviewed-by: Andrei Lepikhov <lepihov@gmail.com> Discussion: https://postgr.es/m/CALDaNm3=JrucjhiiwsYQw5-PGtBHFONa6F7hhWCXMsGvh=tamA@mail.gmail.com
This commit is contained in:
parent
fe08113aef
commit
fd366065e0
23 changed files with 1073 additions and 126 deletions
|
|
@ -6572,6 +6572,16 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
|
|||
</para></entry>
|
||||
</row>
|
||||
|
||||
<row>
|
||||
<entry role="catalog_table_entry"><para role="column_definition">
|
||||
<structfield>prexcept</structfield> <type>bool</type>
|
||||
</para>
|
||||
<para>
|
||||
True if the table is excluded from the publication. See
|
||||
<link linkend="sql-createpublication-params-for-except-table"><literal>EXCEPT TABLE</literal></link>.
|
||||
</para></entry>
|
||||
</row>
|
||||
|
||||
<row>
|
||||
<entry role="catalog_table_entry"><para role="column_definition">
|
||||
<structfield>prqual</structfield> <type>pg_node_tree</type>
|
||||
|
|
|
|||
|
|
@ -116,7 +116,11 @@
|
|||
<literal>FOR TABLES IN SCHEMA</literal>, <literal>FOR ALL TABLES</literal>,
|
||||
or <literal>FOR ALL SEQUENCES</literal>. Unlike tables, sequences can be
|
||||
synchronized at any time. For more information, see
|
||||
<xref linkend="logical-replication-sequences"/>.
|
||||
<xref linkend="logical-replication-sequences"/>. When a publication is
|
||||
created with <literal>FOR ALL TABLES</literal>, a table or set of tables can
|
||||
be explicitly excluded from publication using the
|
||||
<link linkend="sql-createpublication-params-for-except-table"><literal>EXCEPT TABLE</literal></link>
|
||||
clause.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
|
|
|
|||
|
|
@ -32,12 +32,16 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
|
|||
|
||||
<phrase>and <replaceable class="parameter">publication_all_object</replaceable> is one of:</phrase>
|
||||
|
||||
ALL TABLES
|
||||
ALL TABLES [ EXCEPT TABLE ( <replaceable class="parameter">except_table_object</replaceable> [, ... ] ) ]
|
||||
ALL SEQUENCES
|
||||
|
||||
<phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
|
||||
|
||||
[ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] [ WHERE ( <replaceable class="parameter">expression</replaceable> ) ]
|
||||
|
||||
<phrase>and <replaceable class="parameter">except_table_object</replaceable> is:</phrase>
|
||||
|
||||
[ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
|
||||
</synopsis>
|
||||
</refsynopsisdiv>
|
||||
|
||||
|
|
@ -164,7 +168,8 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
|
|||
<listitem>
|
||||
<para>
|
||||
Marks the publication as one that replicates changes for all tables in
|
||||
the database, including tables created in the future.
|
||||
the database, including tables created in the future. Tables listed in
|
||||
EXCEPT TABLE are excluded from the publication.
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
|
|
@ -184,6 +189,37 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
|
|||
</listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry id="sql-createpublication-params-for-except-table">
|
||||
<term><literal>EXCEPT TABLE</literal></term>
|
||||
<listitem>
|
||||
<para>
|
||||
This clause specifies a list of tables to be excluded from the
|
||||
publication.
|
||||
</para>
|
||||
<para>
|
||||
For inherited tables, if <literal>ONLY</literal> is specified before the
|
||||
table name, only that table is excluded from the publication. If
|
||||
<literal>ONLY</literal> is not specified, the table and all its descendant
|
||||
tables (if any) are excluded. Optionally, <literal>*</literal> can be
|
||||
specified after the table name to explicitly indicate that descendant
|
||||
tables are excluded.
|
||||
</para>
|
||||
<para>
|
||||
For partitioned tables, only the root partitioned table may be specified
|
||||
in <literal>EXCEPT TABLE</literal>. Doing so excludes the root table and
|
||||
all of its partitions from replication. The optional
|
||||
<literal>ONLY</literal> and <literal>*</literal> has no effect for
|
||||
partitioned tables.
|
||||
</para>
|
||||
<para>
|
||||
There can be a case where a subscription includes multiple publications.
|
||||
In such a case, a table or partition that is included in one publication
|
||||
and listed in the <literal>EXCEPT TABLE</literal> clause of another is
|
||||
considered included for replication.
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry id="sql-createpublication-params-with">
|
||||
<term><literal>WITH ( <replaceable class="parameter">publication_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )</literal></term>
|
||||
<listitem>
|
||||
|
|
@ -489,6 +525,23 @@ CREATE PUBLICATION all_sequences FOR ALL SEQUENCES;
|
|||
all sequences for synchronization:
|
||||
<programlisting>
|
||||
CREATE PUBLICATION all_tables_sequences FOR ALL TABLES, ALL SEQUENCES;
|
||||
</programlisting>
|
||||
</para>
|
||||
|
||||
<para>
|
||||
Create a publication that publishes all changes in all tables except
|
||||
<structname>users</structname> and <structname>departments</structname>:
|
||||
<programlisting>
|
||||
CREATE PUBLICATION all_tables_except FOR ALL TABLES EXCEPT TABLE (users, departments);
|
||||
</programlisting>
|
||||
</para>
|
||||
|
||||
<para>
|
||||
Create a publication that publishes all sequences for synchronization, and
|
||||
all changes in all tables except <structname>users</structname> and
|
||||
<structname>departments</structname>:
|
||||
<programlisting>
|
||||
CREATE PUBLICATION all_sequences_tables_except FOR ALL SEQUENCES, ALL TABLES EXCEPT TABLE (users, departments);
|
||||
</programlisting>
|
||||
</para>
|
||||
</refsect1>
|
||||
|
|
|
|||
|
|
@ -2103,8 +2103,9 @@ SELECT $1 \parse stmt1
|
|||
listed.
|
||||
If <literal>x</literal> is appended to the command name, the results
|
||||
are displayed in expanded mode.
|
||||
If <literal>+</literal> is appended to the command name, the tables and
|
||||
schemas associated with each publication are shown as well.
|
||||
If <literal>+</literal> is appended to the command name, the tables,
|
||||
excluded tables, and schemas associated with each publication are shown
|
||||
as well.
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
|
|
|
|||
|
|
@ -53,37 +53,48 @@ typedef struct
|
|||
* error if not.
|
||||
*/
|
||||
static void
|
||||
check_publication_add_relation(Relation targetrel)
|
||||
check_publication_add_relation(PublicationRelInfo *pri)
|
||||
{
|
||||
Relation targetrel = pri->relation;
|
||||
const char *errormsg;
|
||||
|
||||
if (pri->except)
|
||||
errormsg = gettext_noop("cannot use publication EXCEPT clause for relation \"%s\"");
|
||||
else
|
||||
errormsg = gettext_noop("cannot add relation \"%s\" to publication");
|
||||
|
||||
/* If in EXCEPT clause, must be root partitioned table */
|
||||
if (pri->except && targetrel->rd_rel->relispartition)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||
errmsg(errormsg, RelationGetRelationName(targetrel)),
|
||||
errdetail("This operation is not supported for individual partitions.")));
|
||||
|
||||
/* Must be a regular or partitioned table */
|
||||
if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION &&
|
||||
RelationGetForm(targetrel)->relkind != RELKIND_PARTITIONED_TABLE)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||
errmsg("cannot add relation \"%s\" to publication",
|
||||
RelationGetRelationName(targetrel)),
|
||||
errmsg(errormsg, RelationGetRelationName(targetrel)),
|
||||
errdetail_relkind_not_supported(RelationGetForm(targetrel)->relkind)));
|
||||
|
||||
/* Can't be system table */
|
||||
if (IsCatalogRelation(targetrel))
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||
errmsg("cannot add relation \"%s\" to publication",
|
||||
RelationGetRelationName(targetrel)),
|
||||
errmsg(errormsg, RelationGetRelationName(targetrel)),
|
||||
errdetail("This operation is not supported for system tables.")));
|
||||
|
||||
/* UNLOGGED and TEMP relations cannot be part of publication. */
|
||||
if (targetrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||
errmsg("cannot add relation \"%s\" to publication",
|
||||
RelationGetRelationName(targetrel)),
|
||||
errmsg(errormsg, RelationGetRelationName(targetrel)),
|
||||
errdetail("This operation is not supported for temporary tables.")));
|
||||
else if (targetrel->rd_rel->relpersistence == RELPERSISTENCE_UNLOGGED)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||
errmsg("cannot add relation \"%s\" to publication",
|
||||
RelationGetRelationName(targetrel)),
|
||||
errmsg(errormsg, RelationGetRelationName(targetrel)),
|
||||
errdetail("This operation is not supported for unlogged tables.")));
|
||||
}
|
||||
|
||||
|
|
@ -366,7 +377,7 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
|
|||
foreach(lc, ancestors)
|
||||
{
|
||||
Oid ancestor = lfirst_oid(lc);
|
||||
List *apubids = GetRelationPublications(ancestor);
|
||||
List *apubids = GetRelationIncludedPublications(ancestor);
|
||||
List *aschemaPubids = NIL;
|
||||
|
||||
level++;
|
||||
|
|
@ -466,7 +477,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
|
|||
RelationGetRelationName(targetrel), pub->name)));
|
||||
}
|
||||
|
||||
check_publication_add_relation(targetrel);
|
||||
check_publication_add_relation(pri);
|
||||
|
||||
/* Validate and translate column names into a Bitmapset of attnums. */
|
||||
attnums = pub_collist_validate(pri->relation, pri->columns);
|
||||
|
|
@ -482,6 +493,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
|
|||
ObjectIdGetDatum(pubid);
|
||||
values[Anum_pg_publication_rel_prrelid - 1] =
|
||||
ObjectIdGetDatum(relid);
|
||||
values[Anum_pg_publication_rel_prexcept - 1] =
|
||||
BoolGetDatum(pri->except);
|
||||
|
||||
/* Add qualifications, if available */
|
||||
if (pri->whereClause != NULL)
|
||||
|
|
@ -530,17 +543,26 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
|
|||
table_close(rel, RowExclusiveLock);
|
||||
|
||||
/*
|
||||
* Invalidate relcache so that publication info is rebuilt.
|
||||
*
|
||||
* For the partitioned tables, we must invalidate all partitions contained
|
||||
* in the respective partition hierarchies, not just the one explicitly
|
||||
* mentioned in the publication. This is required because we implicitly
|
||||
* publish the child tables when the parent table is published.
|
||||
* Relations excluded via the EXCEPT clause do not need explicit
|
||||
* invalidation as CreatePublication() function invalidates all relations
|
||||
* as part of defining a FOR ALL TABLES publication.
|
||||
*/
|
||||
relids = GetPubPartitionOptionRelations(relids, PUBLICATION_PART_ALL,
|
||||
relid);
|
||||
if (!pri->except)
|
||||
{
|
||||
/*
|
||||
* Invalidate relcache so that publication info is rebuilt.
|
||||
*
|
||||
* For the partitioned tables, we must invalidate all partitions
|
||||
* contained in the respective partition hierarchies, not just the one
|
||||
* explicitly mentioned in the publication. This is required because
|
||||
* we implicitly publish the child tables when the parent table is
|
||||
* published.
|
||||
*/
|
||||
relids = GetPubPartitionOptionRelations(relids, PUBLICATION_PART_ALL,
|
||||
relid);
|
||||
|
||||
InvalidatePublicationRels(relids);
|
||||
InvalidatePublicationRels(relids);
|
||||
}
|
||||
|
||||
return myself;
|
||||
}
|
||||
|
|
@ -749,23 +771,30 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
|
|||
return myself;
|
||||
}
|
||||
|
||||
/* Gets list of publication oids for a relation */
|
||||
List *
|
||||
GetRelationPublications(Oid relid)
|
||||
/*
|
||||
* Internal function to get the list of publication oids for a relation.
|
||||
*
|
||||
* If except_flag is true, returns the list of publication that specified the
|
||||
* relation in EXCEPT clause; otherwise, returns the list of publications
|
||||
* in which relation is included.
|
||||
*/
|
||||
static List *
|
||||
get_relation_publications(Oid relid, bool except_flag)
|
||||
{
|
||||
List *result = NIL;
|
||||
CatCList *pubrellist;
|
||||
int i;
|
||||
|
||||
/* Find all publications associated with the relation. */
|
||||
pubrellist = SearchSysCacheList1(PUBLICATIONRELMAP,
|
||||
ObjectIdGetDatum(relid));
|
||||
for (i = 0; i < pubrellist->n_members; i++)
|
||||
for (int i = 0; i < pubrellist->n_members; i++)
|
||||
{
|
||||
HeapTuple tup = &pubrellist->members[i]->tuple;
|
||||
Oid pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid;
|
||||
Form_pg_publication_rel pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
|
||||
Oid pubid = pubrel->prpubid;
|
||||
|
||||
result = lappend_oid(result, pubid);
|
||||
if (pubrel->prexcept == except_flag)
|
||||
result = lappend_oid(result, pubid);
|
||||
}
|
||||
|
||||
ReleaseSysCacheList(pubrellist);
|
||||
|
|
@ -774,13 +803,33 @@ GetRelationPublications(Oid relid)
|
|||
}
|
||||
|
||||
/*
|
||||
* Gets list of relation oids for a publication.
|
||||
*
|
||||
* This should only be used FOR TABLE publications, the FOR ALL TABLES/SEQUENCES
|
||||
* should use GetAllPublicationRelations().
|
||||
* Gets list of publication oids for a relation.
|
||||
*/
|
||||
List *
|
||||
GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
|
||||
GetRelationIncludedPublications(Oid relid)
|
||||
{
|
||||
return get_relation_publications(relid, false);
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets list of publication oids which has relation in EXCEPT clause.
|
||||
*/
|
||||
List *
|
||||
GetRelationExcludedPublications(Oid relid)
|
||||
{
|
||||
return get_relation_publications(relid, true);
|
||||
}
|
||||
|
||||
/*
|
||||
* Internal function to get the list of relation oids for a publication.
|
||||
*
|
||||
* If except_flag is true, returns the list of relations specified in the
|
||||
* EXCEPT clause of the publication; otherwise, returns the list of relations
|
||||
* included in the publication.
|
||||
*/
|
||||
static List *
|
||||
get_publication_relations(Oid pubid, PublicationPartOpt pub_partopt,
|
||||
bool except_flag)
|
||||
{
|
||||
List *result;
|
||||
Relation pubrelsrel;
|
||||
|
|
@ -805,8 +854,10 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
|
|||
Form_pg_publication_rel pubrel;
|
||||
|
||||
pubrel = (Form_pg_publication_rel) GETSTRUCT(tup);
|
||||
result = GetPubPartitionOptionRelations(result, pub_partopt,
|
||||
pubrel->prrelid);
|
||||
|
||||
if (except_flag == pubrel->prexcept)
|
||||
result = GetPubPartitionOptionRelations(result, pub_partopt,
|
||||
pubrel->prrelid);
|
||||
}
|
||||
|
||||
systable_endscan(scan);
|
||||
|
|
@ -819,6 +870,34 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
|
|||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets list of relation oids that are associated with a publication.
|
||||
*
|
||||
* This should only be used FOR TABLE publications, the FOR ALL TABLES/SEQUENCES
|
||||
* should use GetAllPublicationRelations().
|
||||
*/
|
||||
List *
|
||||
GetIncludedPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt)
|
||||
{
|
||||
Assert(!GetPublication(pubid)->alltables);
|
||||
|
||||
return get_publication_relations(pubid, pub_partopt, false);
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets list of table oids that were specified in the EXCEPT clause for a
|
||||
* publication.
|
||||
*
|
||||
* This should only be used FOR ALL TABLES publications.
|
||||
*/
|
||||
List *
|
||||
GetExcludedPublicationTables(Oid pubid, PublicationPartOpt pub_partopt)
|
||||
{
|
||||
Assert(GetPublication(pubid)->alltables);
|
||||
|
||||
return get_publication_relations(pubid, pub_partopt, true);
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets list of publication oids for publications marked as FOR ALL TABLES.
|
||||
*/
|
||||
|
|
@ -858,24 +937,34 @@ GetAllTablesPublications(void)
|
|||
|
||||
/*
|
||||
* Gets list of all relations published by FOR ALL TABLES/SEQUENCES
|
||||
* publication(s).
|
||||
* publication.
|
||||
*
|
||||
* If the publication publishes partition changes via their respective root
|
||||
* partitioned tables, we must exclude partitions in favor of including the
|
||||
* root partitioned tables. This is not applicable to FOR ALL SEQUENCES
|
||||
* publication.
|
||||
*
|
||||
* For a FOR ALL TABLES publication, the returned list excludes tables mentioned
|
||||
* in EXCEPT TABLE clause.
|
||||
*/
|
||||
List *
|
||||
GetAllPublicationRelations(char relkind, bool pubviaroot)
|
||||
GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot)
|
||||
{
|
||||
Relation classRel;
|
||||
ScanKeyData key[1];
|
||||
TableScanDesc scan;
|
||||
HeapTuple tuple;
|
||||
List *result = NIL;
|
||||
List *exceptlist = NIL;
|
||||
|
||||
Assert(!(relkind == RELKIND_SEQUENCE && pubviaroot));
|
||||
|
||||
/* EXCEPT filtering applies only to relations, not sequences */
|
||||
if (relkind == RELKIND_RELATION)
|
||||
exceptlist = GetExcludedPublicationTables(pubid, pubviaroot ?
|
||||
PUBLICATION_PART_ROOT :
|
||||
PUBLICATION_PART_LEAF);
|
||||
|
||||
classRel = table_open(RelationRelationId, AccessShareLock);
|
||||
|
||||
ScanKeyInit(&key[0],
|
||||
|
|
@ -891,7 +980,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
|
|||
Oid relid = relForm->oid;
|
||||
|
||||
if (is_publishable_class(relid, relForm) &&
|
||||
!(relForm->relispartition && pubviaroot))
|
||||
!(relForm->relispartition && pubviaroot) &&
|
||||
!list_member_oid(exceptlist, relid))
|
||||
result = lappend_oid(result, relid);
|
||||
}
|
||||
|
||||
|
|
@ -912,7 +1002,8 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
|
|||
Oid relid = relForm->oid;
|
||||
|
||||
if (is_publishable_class(relid, relForm) &&
|
||||
!relForm->relispartition)
|
||||
!relForm->relispartition &&
|
||||
!list_member_oid(exceptlist, relid))
|
||||
result = lappend_oid(result, relid);
|
||||
}
|
||||
|
||||
|
|
@ -1168,17 +1259,18 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
|
|||
* those. Otherwise, get the partitioned table itself.
|
||||
*/
|
||||
if (pub_elem->alltables)
|
||||
pub_elem_tables = GetAllPublicationRelations(RELKIND_RELATION,
|
||||
pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
|
||||
RELKIND_RELATION,
|
||||
pub_elem->pubviaroot);
|
||||
else
|
||||
{
|
||||
List *relids,
|
||||
*schemarelids;
|
||||
|
||||
relids = GetPublicationRelations(pub_elem->oid,
|
||||
pub_elem->pubviaroot ?
|
||||
PUBLICATION_PART_ROOT :
|
||||
PUBLICATION_PART_LEAF);
|
||||
relids = GetIncludedPublicationRelations(pub_elem->oid,
|
||||
pub_elem->pubviaroot ?
|
||||
PUBLICATION_PART_ROOT :
|
||||
PUBLICATION_PART_LEAF);
|
||||
schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
|
||||
pub_elem->pubviaroot ?
|
||||
PUBLICATION_PART_ROOT :
|
||||
|
|
@ -1367,7 +1459,9 @@ pg_get_publication_sequences(PG_FUNCTION_ARGS)
|
|||
publication = GetPublicationByName(pubname, false);
|
||||
|
||||
if (publication->allsequences)
|
||||
sequences = GetAllPublicationRelations(RELKIND_SEQUENCE, false);
|
||||
sequences = GetAllPublicationRelations(publication->oid,
|
||||
RELKIND_SEQUENCE,
|
||||
false);
|
||||
|
||||
funcctx->user_fctx = sequences;
|
||||
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ parse_publication_options(ParseState *pstate,
|
|||
*/
|
||||
static void
|
||||
ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
|
||||
List **rels, List **schemas)
|
||||
List **rels, List **exceptrels, List **schemas)
|
||||
{
|
||||
ListCell *cell;
|
||||
PublicationObjSpec *pubobj;
|
||||
|
|
@ -198,7 +198,12 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate,
|
|||
|
||||
switch (pubobj->pubobjtype)
|
||||
{
|
||||
case PUBLICATIONOBJ_EXCEPT_TABLE:
|
||||
pubobj->pubtable->except = true;
|
||||
*exceptrels = lappend(*exceptrels, pubobj->pubtable);
|
||||
break;
|
||||
case PUBLICATIONOBJ_TABLE:
|
||||
pubobj->pubtable->except = false;
|
||||
*rels = lappend(*rels, pubobj->pubtable);
|
||||
break;
|
||||
case PUBLICATIONOBJ_TABLES_IN_SCHEMA:
|
||||
|
|
@ -519,8 +524,8 @@ InvalidatePubRelSyncCache(Oid pubid, bool puballtables)
|
|||
* a target. However, WAL records for TRUNCATE specify both a root and
|
||||
* its leaves.
|
||||
*/
|
||||
relids = GetPublicationRelations(pubid,
|
||||
PUBLICATION_PART_ALL);
|
||||
relids = GetIncludedPublicationRelations(pubid,
|
||||
PUBLICATION_PART_ALL);
|
||||
schemarelids = GetAllSchemaPublicationRelations(pubid,
|
||||
PUBLICATION_PART_ALL);
|
||||
|
||||
|
|
@ -844,6 +849,7 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
|
|||
char publish_generated_columns;
|
||||
AclResult aclresult;
|
||||
List *relations = NIL;
|
||||
List *exceptrelations = NIL;
|
||||
List *schemaidlist = NIL;
|
||||
|
||||
/* must have CREATE privilege on database */
|
||||
|
|
@ -929,8 +935,21 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
|
|||
CommandCounterIncrement();
|
||||
|
||||
/* Associate objects with the publication. */
|
||||
ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
|
||||
&exceptrelations, &schemaidlist);
|
||||
|
||||
if (stmt->for_all_tables)
|
||||
{
|
||||
/* Process EXCEPT table list */
|
||||
if (exceptrelations != NIL)
|
||||
{
|
||||
List *rels;
|
||||
|
||||
rels = OpenTableList(exceptrelations);
|
||||
PublicationAddTables(puboid, rels, true, NULL);
|
||||
CloseTableList(rels);
|
||||
}
|
||||
|
||||
/*
|
||||
* Invalidate relcache so that publication info is rebuilt. Sequences
|
||||
* publication doesn't require invalidation, as replica identity
|
||||
|
|
@ -940,9 +959,6 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
|
|||
}
|
||||
else if (!stmt->for_all_sequences)
|
||||
{
|
||||
ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
|
||||
&schemaidlist);
|
||||
|
||||
/* FOR TABLES IN SCHEMA requires superuser */
|
||||
if (schemaidlist != NIL && !superuser())
|
||||
ereport(ERROR,
|
||||
|
|
@ -1050,8 +1066,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
|
|||
LockDatabaseObject(PublicationRelationId, pubform->oid, 0,
|
||||
AccessShareLock);
|
||||
|
||||
root_relids = GetPublicationRelations(pubform->oid,
|
||||
PUBLICATION_PART_ROOT);
|
||||
root_relids = GetIncludedPublicationRelations(pubform->oid,
|
||||
PUBLICATION_PART_ROOT);
|
||||
|
||||
foreach(lc, root_relids)
|
||||
{
|
||||
|
|
@ -1170,8 +1186,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
|
|||
* trees, not just those explicitly mentioned in the publication.
|
||||
*/
|
||||
if (root_relids == NIL)
|
||||
relids = GetPublicationRelations(pubform->oid,
|
||||
PUBLICATION_PART_ALL);
|
||||
relids = GetIncludedPublicationRelations(pubform->oid,
|
||||
PUBLICATION_PART_ALL);
|
||||
else
|
||||
{
|
||||
/*
|
||||
|
|
@ -1256,8 +1272,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
|
|||
PublicationDropTables(pubid, rels, false);
|
||||
else /* AP_SetObjects */
|
||||
{
|
||||
List *oldrelids = GetPublicationRelations(pubid,
|
||||
PUBLICATION_PART_ROOT);
|
||||
List *oldrelids = GetIncludedPublicationRelations(pubid,
|
||||
PUBLICATION_PART_ROOT);
|
||||
List *delrels = NIL;
|
||||
ListCell *oldlc;
|
||||
|
||||
|
|
@ -1358,6 +1374,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
|
|||
oldrel = palloc_object(PublicationRelInfo);
|
||||
oldrel->whereClause = NULL;
|
||||
oldrel->columns = NIL;
|
||||
oldrel->except = false;
|
||||
oldrel->relation = table_open(oldrelid,
|
||||
ShareUpdateExclusiveLock);
|
||||
delrels = lappend(delrels, oldrel);
|
||||
|
|
@ -1408,7 +1425,8 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
|
|||
ListCell *lc;
|
||||
List *reloids;
|
||||
|
||||
reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
|
||||
reloids = GetIncludedPublicationRelations(pubform->oid,
|
||||
PUBLICATION_PART_ROOT);
|
||||
|
||||
foreach(lc, reloids)
|
||||
{
|
||||
|
|
@ -1566,11 +1584,15 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
|
|||
else
|
||||
{
|
||||
List *relations = NIL;
|
||||
List *exceptrelations = NIL;
|
||||
List *schemaidlist = NIL;
|
||||
Oid pubid = pubform->oid;
|
||||
|
||||
ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
|
||||
&schemaidlist);
|
||||
&exceptrelations, &schemaidlist);
|
||||
|
||||
/* EXCEPT clause is not supported with ALTER PUBLICATION */
|
||||
Assert(exceptrelations == NIL);
|
||||
|
||||
CheckAlterPublication(stmt, tup, relations, schemaidlist);
|
||||
|
||||
|
|
@ -1771,6 +1793,7 @@ OpenTableList(List *tables)
|
|||
pub_rel->relation = rel;
|
||||
pub_rel->whereClause = t->whereClause;
|
||||
pub_rel->columns = t->columns;
|
||||
pub_rel->except = t->except;
|
||||
rels = lappend(rels, pub_rel);
|
||||
relids = lappend_oid(relids, myrelid);
|
||||
|
||||
|
|
@ -1843,6 +1866,7 @@ OpenTableList(List *tables)
|
|||
|
||||
/* child inherits column list from parent */
|
||||
pub_rel->columns = t->columns;
|
||||
pub_rel->except = t->except;
|
||||
rels = lappend(rels, pub_rel);
|
||||
relids = lappend_oid(relids, childrelid);
|
||||
|
||||
|
|
|
|||
|
|
@ -8686,7 +8686,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
|
|||
* expressions.
|
||||
*/
|
||||
if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
|
||||
GetRelationPublications(RelationGetRelid(rel)) != NIL)
|
||||
GetRelationIncludedPublications(RelationGetRelid(rel)) != NIL)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"),
|
||||
|
|
@ -18881,7 +18881,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
|
|||
* UNLOGGED, as UNLOGGED tables can't be published.
|
||||
*/
|
||||
if (!toLogged &&
|
||||
GetRelationPublications(RelationGetRelid(rel)) != NIL)
|
||||
GetRelationIncludedPublications(RelationGetRelid(rel)) != NIL)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
|
||||
errmsg("cannot change table \"%s\" to unlogged because it is part of a publication",
|
||||
|
|
@ -20322,6 +20322,7 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
|
|||
const char *trigger_name;
|
||||
Oid defaultPartOid;
|
||||
List *partBoundConstraint;
|
||||
List *exceptpuboids = NIL;
|
||||
ParseState *pstate = make_parsestate(NULL);
|
||||
|
||||
pstate->p_sourcetext = context->queryString;
|
||||
|
|
@ -20361,6 +20362,49 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
|
|||
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
|
||||
errmsg("cannot attach a typed table as partition")));
|
||||
|
||||
/*
|
||||
* Disallow attaching a partition if the table is referenced in a
|
||||
* publication EXCEPT clause. Changing the partition hierarchy could alter
|
||||
* the effective publication membership.
|
||||
*/
|
||||
exceptpuboids = GetRelationExcludedPublications(RelationGetRelid(attachrel));
|
||||
if (exceptpuboids != NIL)
|
||||
{
|
||||
bool first = true;
|
||||
StringInfoData pubnames;
|
||||
|
||||
initStringInfo(&pubnames);
|
||||
|
||||
foreach_oid(pubid, exceptpuboids)
|
||||
{
|
||||
char *pubname = get_publication_name(pubid, false);
|
||||
|
||||
if (!first)
|
||||
{
|
||||
/*
|
||||
* translator: This is a separator in a list of publication
|
||||
* names.
|
||||
*/
|
||||
appendStringInfoString(&pubnames, _(", "));
|
||||
}
|
||||
|
||||
first = false;
|
||||
|
||||
appendStringInfo(&pubnames, _("\"%s\""), pubname);
|
||||
}
|
||||
|
||||
ereport(ERROR,
|
||||
errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
|
||||
errmsg_plural("cannot attach table \"%s\" as partition because it is referenced in publication %s EXCEPT clause",
|
||||
"cannot attach table \"%s\" as partition because it is referenced in publications %s EXCEPT clause",
|
||||
list_length(exceptpuboids),
|
||||
RelationGetRelationName(attachrel),
|
||||
pubnames.data),
|
||||
errdetail("The publication EXCEPT clause cannot contain tables that are partitions."));
|
||||
}
|
||||
|
||||
list_free(exceptpuboids);
|
||||
|
||||
/*
|
||||
* Table being attached should not already be part of inheritance; either
|
||||
* as a child table...
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@ static void processCASbits(int cas_bits, int location, const char *constrType,
|
|||
static PartitionStrategy parsePartitionStrategy(char *strategy, int location,
|
||||
core_yyscan_t yyscanner);
|
||||
static void preprocess_pub_all_objtype_list(List *all_objects_list,
|
||||
List **pubobjects,
|
||||
bool *all_tables,
|
||||
bool *all_sequences,
|
||||
core_yyscan_t yyscanner);
|
||||
|
|
@ -455,6 +456,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
|
|||
TriggerTransitions TriggerReferencing
|
||||
vacuum_relation_list opt_vacuum_relation_list
|
||||
drop_option_list pub_obj_list pub_all_obj_type_list
|
||||
pub_except_obj_list opt_pub_except_clause
|
||||
|
||||
%type <retclause> returning_clause
|
||||
%type <node> returning_option
|
||||
|
|
@ -592,6 +594,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
|
|||
%type <node> var_value zone_value
|
||||
%type <rolespec> auth_ident RoleSpec opt_granted_by
|
||||
%type <publicationobjectspec> PublicationObjSpec
|
||||
%type <publicationobjectspec> PublicationExceptObjSpec
|
||||
%type <publicationallobjectspec> PublicationAllObjSpec
|
||||
|
||||
%type <keyword> unreserved_keyword type_func_name_keyword
|
||||
|
|
@ -10792,7 +10795,7 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
|
|||
*
|
||||
* pub_all_obj_type is one of:
|
||||
*
|
||||
* TABLES
|
||||
* TABLES [EXCEPT TABLE ( table [, ...] )]
|
||||
* SEQUENCES
|
||||
*
|
||||
* CREATE PUBLICATION FOR pub_obj [, ...] [WITH options]
|
||||
|
|
@ -10818,7 +10821,8 @@ CreatePublicationStmt:
|
|||
CreatePublicationStmt *n = makeNode(CreatePublicationStmt);
|
||||
|
||||
n->pubname = $3;
|
||||
preprocess_pub_all_objtype_list($5, &n->for_all_tables,
|
||||
preprocess_pub_all_objtype_list($5, &n->pubobjects,
|
||||
&n->for_all_tables,
|
||||
&n->for_all_sequences,
|
||||
yyscanner);
|
||||
n->options = $6;
|
||||
|
|
@ -10933,11 +10937,17 @@ pub_obj_list: PublicationObjSpec
|
|||
{ $$ = lappend($1, $3); }
|
||||
;
|
||||
|
||||
opt_pub_except_clause:
|
||||
EXCEPT TABLE '(' pub_except_obj_list ')' { $$ = $4; }
|
||||
| /*EMPTY*/ { $$ = NIL; }
|
||||
;
|
||||
|
||||
PublicationAllObjSpec:
|
||||
ALL TABLES
|
||||
ALL TABLES opt_pub_except_clause
|
||||
{
|
||||
$$ = makeNode(PublicationAllObjSpec);
|
||||
$$->pubobjtype = PUBLICATION_ALL_TABLES;
|
||||
$$->except_tables = $3;
|
||||
$$->location = @1;
|
||||
}
|
||||
| ALL SEQUENCES
|
||||
|
|
@ -10954,6 +10964,23 @@ pub_all_obj_type_list: PublicationAllObjSpec
|
|||
{ $$ = lappend($1, $3); }
|
||||
;
|
||||
|
||||
PublicationExceptObjSpec:
|
||||
relation_expr
|
||||
{
|
||||
$$ = makeNode(PublicationObjSpec);
|
||||
$$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE;
|
||||
$$->pubtable = makeNode(PublicationTable);
|
||||
$$->pubtable->except = true;
|
||||
$$->pubtable->relation = $1;
|
||||
$$->location = @1;
|
||||
}
|
||||
;
|
||||
|
||||
pub_except_obj_list: PublicationExceptObjSpec
|
||||
{ $$ = list_make1($1); }
|
||||
| pub_except_obj_list ',' PublicationExceptObjSpec
|
||||
{ $$ = lappend($1, $3); }
|
||||
;
|
||||
|
||||
/*****************************************************************************
|
||||
*
|
||||
|
|
@ -19812,8 +19839,9 @@ parsePartitionStrategy(char *strategy, int location, core_yyscan_t yyscanner)
|
|||
* Also, checks if the pub_object_type has been specified more than once.
|
||||
*/
|
||||
static void
|
||||
preprocess_pub_all_objtype_list(List *all_objects_list, bool *all_tables,
|
||||
bool *all_sequences, core_yyscan_t yyscanner)
|
||||
preprocess_pub_all_objtype_list(List *all_objects_list, List **pubobjects,
|
||||
bool *all_tables, bool *all_sequences,
|
||||
core_yyscan_t yyscanner)
|
||||
{
|
||||
if (!all_objects_list)
|
||||
return;
|
||||
|
|
@ -19833,6 +19861,7 @@ preprocess_pub_all_objtype_list(List *all_objects_list, bool *all_tables,
|
|||
parser_errposition(obj->location));
|
||||
|
||||
*all_tables = true;
|
||||
*pubobjects = list_concat(*pubobjects, obj->except_tables);
|
||||
}
|
||||
else if (obj->pubobjtype == PUBLICATION_ALL_SEQUENCES)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2089,7 +2089,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
|
|||
if (!entry->replicate_valid)
|
||||
{
|
||||
Oid schemaId = get_rel_namespace(relid);
|
||||
List *pubids = GetRelationPublications(relid);
|
||||
List *pubids = GetRelationIncludedPublications(relid);
|
||||
|
||||
/*
|
||||
* We don't acquire a lock on the namespace system table as we build
|
||||
|
|
@ -2206,14 +2206,47 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
|
|||
*/
|
||||
if (pub->alltables)
|
||||
{
|
||||
publish = true;
|
||||
if (pub->pubviaroot && am_partition)
|
||||
List *exceptpubids = NIL;
|
||||
|
||||
if (am_partition)
|
||||
{
|
||||
List *ancestors = get_partition_ancestors(relid);
|
||||
Oid last_ancestor_relid = llast_oid(ancestors);
|
||||
|
||||
pub_relid = llast_oid(ancestors);
|
||||
ancestor_level = list_length(ancestors);
|
||||
/*
|
||||
* For a partition, changes are published via top-most
|
||||
* ancestor when pubviaroot is true, so populate pub_relid
|
||||
* accordingly.
|
||||
*/
|
||||
if (pub->pubviaroot)
|
||||
{
|
||||
pub_relid = last_ancestor_relid;
|
||||
ancestor_level = list_length(ancestors);
|
||||
}
|
||||
|
||||
/*
|
||||
* Only the top-most ancestor can appear in the EXCEPT
|
||||
* clause. Therefore, for a partition, exclusion must be
|
||||
* evaluated at the top-most ancestor.
|
||||
*/
|
||||
exceptpubids = GetRelationExcludedPublications(last_ancestor_relid);
|
||||
}
|
||||
else
|
||||
{
|
||||
/*
|
||||
* For a regular table or a root partitioned table, check
|
||||
* exclusion on table itself.
|
||||
*/
|
||||
exceptpubids = GetRelationExcludedPublications(pub_relid);
|
||||
}
|
||||
|
||||
if (!list_member_oid(exceptpubids, pub->oid))
|
||||
publish = true;
|
||||
|
||||
list_free(exceptpubids);
|
||||
|
||||
if (!publish)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!publish)
|
||||
|
|
|
|||
33
src/backend/utils/cache/relcache.c
vendored
33
src/backend/utils/cache/relcache.c
vendored
|
|
@ -5788,7 +5788,9 @@ RelationGetExclusionInfo(Relation indexRelation,
|
|||
void
|
||||
RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
|
||||
{
|
||||
List *puboids;
|
||||
List *puboids = NIL;
|
||||
List *exceptpuboids = NIL;
|
||||
List *alltablespuboids;
|
||||
ListCell *lc;
|
||||
MemoryContext oldcxt;
|
||||
Oid schemaid;
|
||||
|
|
@ -5826,28 +5828,49 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
|
|||
pubdesc->gencols_valid_for_delete = true;
|
||||
|
||||
/* Fetch the publication membership info. */
|
||||
puboids = GetRelationPublications(relid);
|
||||
puboids = GetRelationIncludedPublications(relid);
|
||||
schemaid = RelationGetNamespace(relation);
|
||||
puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
|
||||
|
||||
if (relation->rd_rel->relispartition)
|
||||
{
|
||||
Oid last_ancestor_relid;
|
||||
|
||||
/* Add publications that the ancestors are in too. */
|
||||
ancestors = get_partition_ancestors(relid);
|
||||
last_ancestor_relid = llast_oid(ancestors);
|
||||
|
||||
foreach(lc, ancestors)
|
||||
{
|
||||
Oid ancestor = lfirst_oid(lc);
|
||||
|
||||
puboids = list_concat_unique_oid(puboids,
|
||||
GetRelationPublications(ancestor));
|
||||
GetRelationIncludedPublications(ancestor));
|
||||
schemaid = get_rel_namespace(ancestor);
|
||||
puboids = list_concat_unique_oid(puboids,
|
||||
GetSchemaPublications(schemaid));
|
||||
}
|
||||
}
|
||||
puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
|
||||
|
||||
/*
|
||||
* Only the top-most ancestor can appear in the EXCEPT clause.
|
||||
* Therefore, for a partition, exclusion must be evaluated at the
|
||||
* top-most ancestor.
|
||||
*/
|
||||
exceptpuboids = GetRelationExcludedPublications(last_ancestor_relid);
|
||||
}
|
||||
else
|
||||
{
|
||||
/*
|
||||
* For a regular table or a root partitioned table, check exclusion on
|
||||
* table itself.
|
||||
*/
|
||||
exceptpuboids = GetRelationExcludedPublications(relid);
|
||||
}
|
||||
|
||||
alltablespuboids = GetAllTablesPublications();
|
||||
puboids = list_concat_unique_oid(puboids,
|
||||
list_difference_oid(alltablespuboids,
|
||||
exceptpuboids));
|
||||
foreach(lc, puboids)
|
||||
{
|
||||
Oid pubid = lfirst_oid(lc);
|
||||
|
|
|
|||
|
|
@ -4623,9 +4623,59 @@ getPublications(Archive *fout)
|
|||
(strcmp(PQgetvalue(res, i, i_pubviaroot), "t") == 0);
|
||||
pubinfo[i].pubgencols_type =
|
||||
*(PQgetvalue(res, i, i_pubgencols));
|
||||
pubinfo[i].except_tables = (SimplePtrList)
|
||||
{
|
||||
NULL, NULL
|
||||
};
|
||||
|
||||
/* Decide whether we want to dump it */
|
||||
selectDumpableObject(&(pubinfo[i].dobj), fout);
|
||||
|
||||
/*
|
||||
* Get the list of tables for publications specified in the EXCEPT
|
||||
* TABLE clause.
|
||||
*
|
||||
* Although individual EXCEPT TABLE entries could be stored in
|
||||
* PublicationRelInfo, dumpPublicationTable cannot be used to emit
|
||||
* them, because there is no ALTER PUBLICATION ... ADD command to add
|
||||
* individual table entries to the EXCEPT TABLE list.
|
||||
*
|
||||
* Therefore, the approach is to dump the complete EXCEPT TABLE list
|
||||
* in a single CREATE PUBLICATION statement. PublicationInfo is used
|
||||
* to collect this information, which is then emitted by
|
||||
* dumpPublication().
|
||||
*/
|
||||
if (fout->remoteVersion >= 190000)
|
||||
{
|
||||
int ntbls;
|
||||
PGresult *res_tbls;
|
||||
|
||||
resetPQExpBuffer(query);
|
||||
appendPQExpBuffer(query,
|
||||
"SELECT prrelid\n"
|
||||
"FROM pg_catalog.pg_publication_rel\n"
|
||||
"WHERE prpubid = %u AND prexcept",
|
||||
pubinfo[i].dobj.catId.oid);
|
||||
|
||||
res_tbls = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
|
||||
|
||||
ntbls = PQntuples(res_tbls);
|
||||
|
||||
for (int j = 0; j < ntbls; j++)
|
||||
{
|
||||
Oid prrelid;
|
||||
TableInfo *tbinfo;
|
||||
|
||||
prrelid = atooid(PQgetvalue(res_tbls, j, 0));
|
||||
|
||||
tbinfo = findTableByOid(prrelid);
|
||||
|
||||
if (tbinfo != NULL)
|
||||
simple_ptr_list_append(&pubinfo[i].except_tables, tbinfo);
|
||||
}
|
||||
|
||||
PQclear(res_tbls);
|
||||
}
|
||||
}
|
||||
|
||||
cleanup:
|
||||
|
|
@ -4662,10 +4712,29 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
|
|||
appendPQExpBuffer(query, "CREATE PUBLICATION %s",
|
||||
qpubname);
|
||||
|
||||
if (pubinfo->puballtables && pubinfo->puballsequences)
|
||||
appendPQExpBufferStr(query, " FOR ALL TABLES, ALL SEQUENCES");
|
||||
else if (pubinfo->puballtables)
|
||||
if (pubinfo->puballtables)
|
||||
{
|
||||
int n_except = 0;
|
||||
|
||||
appendPQExpBufferStr(query, " FOR ALL TABLES");
|
||||
|
||||
/* Include EXCEPT TABLE clause if there are except_tables. */
|
||||
for (SimplePtrListCell *cell = pubinfo->except_tables.head; cell; cell = cell->next)
|
||||
{
|
||||
TableInfo *tbinfo = (TableInfo *) cell->ptr;
|
||||
|
||||
if (++n_except == 1)
|
||||
appendPQExpBufferStr(query, " EXCEPT TABLE (");
|
||||
else
|
||||
appendPQExpBufferStr(query, ", ");
|
||||
appendPQExpBuffer(query, "ONLY %s", fmtQualifiedDumpable(tbinfo));
|
||||
}
|
||||
if (n_except > 0)
|
||||
appendPQExpBufferStr(query, ")");
|
||||
|
||||
if (pubinfo->puballsequences)
|
||||
appendPQExpBufferStr(query, ", ALL SEQUENCES");
|
||||
}
|
||||
else if (pubinfo->puballsequences)
|
||||
appendPQExpBufferStr(query, " FOR ALL SEQUENCES");
|
||||
|
||||
|
|
@ -4845,6 +4914,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
|
|||
|
||||
/* Collect all publication membership info. */
|
||||
if (fout->remoteVersion >= 150000)
|
||||
{
|
||||
appendPQExpBufferStr(query,
|
||||
"SELECT tableoid, oid, prpubid, prrelid, "
|
||||
"pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, "
|
||||
|
|
@ -4857,6 +4927,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables)
|
|||
" WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n"
|
||||
" ELSE NULL END) prattrs "
|
||||
"FROM pg_catalog.pg_publication_rel pr");
|
||||
if (fout->remoteVersion >= 190000)
|
||||
appendPQExpBufferStr(query, " WHERE NOT pr.prexcept");
|
||||
}
|
||||
else
|
||||
appendPQExpBufferStr(query,
|
||||
"SELECT tableoid, oid, prpubid, prrelid, "
|
||||
|
|
|
|||
|
|
@ -676,6 +676,7 @@ typedef struct _PublicationInfo
|
|||
bool pubtruncate;
|
||||
bool pubviaroot;
|
||||
PublishGencolsType pubgencols_type;
|
||||
SimplePtrList except_tables;
|
||||
} PublicationInfo;
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -3202,6 +3202,36 @@ my %tests = (
|
|||
like => { %full_runs, section_post_data => 1, },
|
||||
},
|
||||
|
||||
'CREATE PUBLICATION pub8' => {
|
||||
create_order => 50,
|
||||
create_sql =>
|
||||
'CREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT TABLE (dump_test.test_table);',
|
||||
regexp => qr/^
|
||||
\QCREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table) WITH (publish = 'insert, update, delete, truncate');\E
|
||||
/xm,
|
||||
like => { %full_runs, section_post_data => 1, },
|
||||
},
|
||||
|
||||
'CREATE PUBLICATION pub9' => {
|
||||
create_order => 50,
|
||||
create_sql =>
|
||||
'CREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (dump_test.test_table, dump_test.test_second_table);',
|
||||
regexp => qr/^
|
||||
\QCREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_table, ONLY dump_test.test_second_table) WITH (publish = 'insert, update, delete, truncate');\E
|
||||
/xm,
|
||||
like => { %full_runs, section_post_data => 1, },
|
||||
},
|
||||
|
||||
'CREATE PUBLICATION pub10' => {
|
||||
create_order => 92,
|
||||
create_sql =>
|
||||
'CREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT TABLE (dump_test.test_inheritance_parent);',
|
||||
regexp => qr/^
|
||||
\QCREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT TABLE (ONLY dump_test.test_inheritance_parent, ONLY dump_test.test_inheritance_child) WITH (publish = 'insert, update, delete, truncate');\E
|
||||
/xm,
|
||||
like => { %full_runs, section_post_data => 1, },
|
||||
},
|
||||
|
||||
'CREATE SUBSCRIPTION sub1' => {
|
||||
create_order => 50,
|
||||
create_sql => 'CREATE SUBSCRIPTION sub1
|
||||
|
|
|
|||
|
|
@ -3075,15 +3075,43 @@ describeOneTableDetails(const char *schemaname,
|
|||
"FROM pg_catalog.pg_publication p\n"
|
||||
" JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
|
||||
" JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
|
||||
"WHERE pr.prrelid = '%s'\n"
|
||||
"UNION\n"
|
||||
"SELECT pubname\n"
|
||||
" , NULL\n"
|
||||
" , NULL\n"
|
||||
"FROM pg_catalog.pg_publication p\n"
|
||||
"WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
|
||||
"ORDER BY 1;",
|
||||
oid, oid, oid, oid);
|
||||
"WHERE pr.prrelid = '%s'\n",
|
||||
oid, oid, oid);
|
||||
|
||||
if (pset.sversion >= 190000)
|
||||
{
|
||||
/*
|
||||
* Skip entries where this relation appears in the
|
||||
* publication's EXCEPT TABLE list.
|
||||
*/
|
||||
appendPQExpBuffer(&buf,
|
||||
" AND NOT pr.prexcept\n"
|
||||
"UNION\n"
|
||||
"SELECT pubname\n"
|
||||
" , NULL\n"
|
||||
" , NULL\n"
|
||||
"FROM pg_catalog.pg_publication p\n"
|
||||
"WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
|
||||
" AND NOT EXISTS (\n"
|
||||
" SELECT 1\n"
|
||||
" FROM pg_catalog.pg_publication_rel pr\n"
|
||||
" WHERE pr.prpubid = p.oid AND\n"
|
||||
" (pr.prrelid = '%s' OR pr.prrelid = pg_catalog.pg_partition_root('%s')))\n"
|
||||
"ORDER BY 1;",
|
||||
oid, oid, oid);
|
||||
}
|
||||
else
|
||||
{
|
||||
appendPQExpBuffer(&buf,
|
||||
"UNION\n"
|
||||
"SELECT pubname\n"
|
||||
" , NULL\n"
|
||||
" , NULL\n"
|
||||
"FROM pg_catalog.pg_publication p\n"
|
||||
"WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
|
||||
"ORDER BY 1;",
|
||||
oid);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -3134,6 +3162,36 @@ describeOneTableDetails(const char *schemaname,
|
|||
PQclear(result);
|
||||
}
|
||||
|
||||
/* Print publications where the table is in the EXCEPT clause */
|
||||
if (pset.sversion >= 190000)
|
||||
{
|
||||
printfPQExpBuffer(&buf,
|
||||
"SELECT pubname\n"
|
||||
"FROM pg_catalog.pg_publication p\n"
|
||||
"JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n"
|
||||
"WHERE (pr.prrelid = '%s' OR pr.prrelid = pg_catalog.pg_partition_root('%s'))\n"
|
||||
"AND pr.prexcept\n"
|
||||
"ORDER BY 1;", oid, oid);
|
||||
|
||||
result = PSQLexec(buf.data);
|
||||
if (!result)
|
||||
goto error_return;
|
||||
else
|
||||
tuples = PQntuples(result);
|
||||
|
||||
if (tuples > 0)
|
||||
printTableAddFooter(&cont, _("Except Publications:"));
|
||||
|
||||
/* Might be an empty set - that's ok */
|
||||
for (i = 0; i < tuples; i++)
|
||||
{
|
||||
printfPQExpBuffer(&buf, " \"%s\"", PQgetvalue(result, i, 0));
|
||||
|
||||
printTableAddFooter(&cont, buf.data);
|
||||
}
|
||||
PQclear(result);
|
||||
}
|
||||
|
||||
/*
|
||||
* If verbose, print NOT NULL constraints.
|
||||
*/
|
||||
|
|
@ -6763,8 +6821,12 @@ describePublications(const char *pattern)
|
|||
" pg_catalog.pg_publication_rel pr\n"
|
||||
"WHERE c.relnamespace = n.oid\n"
|
||||
" AND c.oid = pr.prrelid\n"
|
||||
" AND pr.prpubid = '%s'\n"
|
||||
"ORDER BY 1,2", pubid);
|
||||
" AND pr.prpubid = '%s'\n", pubid);
|
||||
|
||||
if (pset.sversion >= 190000)
|
||||
appendPQExpBuffer(&buf, " AND NOT pr.prexcept\n");
|
||||
|
||||
appendPQExpBuffer(&buf, "ORDER BY 1,2");
|
||||
if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont))
|
||||
goto error_return;
|
||||
|
||||
|
|
@ -6782,6 +6844,23 @@ describePublications(const char *pattern)
|
|||
goto error_return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (pset.sversion >= 190000)
|
||||
{
|
||||
/* Get tables in the EXCEPT clause for this publication */
|
||||
printfPQExpBuffer(&buf,
|
||||
"SELECT n.nspname || '.' || c.relname\n"
|
||||
"FROM pg_catalog.pg_class c\n"
|
||||
" JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\n"
|
||||
" JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n"
|
||||
"WHERE pr.prpubid = '%s' AND pr.prexcept\n"
|
||||
"ORDER BY 1", pubid);
|
||||
if (!addFooterToPublicationDesc(&buf, _("Except tables:"),
|
||||
true, &cont))
|
||||
goto error_return;
|
||||
}
|
||||
}
|
||||
|
||||
printTable(&cont, pset.queryFout, false, pset.logfile);
|
||||
printTableCleanup(&cont);
|
||||
|
|
|
|||
|
|
@ -3681,7 +3681,17 @@ match_previous_words(int pattern_id,
|
|||
else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL"))
|
||||
COMPLETE_WITH("TABLES", "SEQUENCES");
|
||||
else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES"))
|
||||
COMPLETE_WITH("WITH (");
|
||||
COMPLETE_WITH("EXCEPT TABLE (", "WITH (");
|
||||
else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT"))
|
||||
COMPLETE_WITH("TABLE (");
|
||||
else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE"))
|
||||
COMPLETE_WITH("(");
|
||||
else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "("))
|
||||
COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
|
||||
else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "(", MatchAnyN) && ends_with(prev_wd, ','))
|
||||
COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
|
||||
else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "TABLE", "(", MatchAnyN) && !ends_with(prev_wd, ','))
|
||||
COMPLETE_WITH(")");
|
||||
else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES"))
|
||||
COMPLETE_WITH("IN SCHEMA");
|
||||
else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ','))
|
||||
|
|
|
|||
|
|
@ -57,6 +57,6 @@
|
|||
*/
|
||||
|
||||
/* yyyymmddN */
|
||||
#define CATALOG_VERSION_NO 202602201
|
||||
#define CATALOG_VERSION_NO 202603041
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -150,16 +150,19 @@ typedef struct PublicationRelInfo
|
|||
Relation relation;
|
||||
Node *whereClause;
|
||||
List *columns;
|
||||
bool except;
|
||||
} PublicationRelInfo;
|
||||
|
||||
extern Publication *GetPublication(Oid pubid);
|
||||
extern Publication *GetPublicationByName(const char *pubname, bool missing_ok);
|
||||
extern List *GetRelationPublications(Oid relid);
|
||||
extern List *GetRelationIncludedPublications(Oid relid);
|
||||
extern List *GetRelationExcludedPublications(Oid relid);
|
||||
|
||||
/*---------
|
||||
* Expected values for pub_partopt parameter of GetPublicationRelations(),
|
||||
* which allows callers to specify which partitions of partitioned tables
|
||||
* mentioned in the publication they expect to see.
|
||||
* Expected values for pub_partopt parameter of
|
||||
* GetIncludedPublicationRelations(), which allows callers to specify which
|
||||
* partitions of partitioned tables mentioned in the publication they expect to
|
||||
* see.
|
||||
*
|
||||
* ROOT: only the table explicitly mentioned in the publication
|
||||
* LEAF: only leaf partitions in given tree
|
||||
|
|
@ -172,9 +175,12 @@ typedef enum PublicationPartOpt
|
|||
PUBLICATION_PART_ALL,
|
||||
} PublicationPartOpt;
|
||||
|
||||
extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt);
|
||||
extern List *GetIncludedPublicationRelations(Oid pubid,
|
||||
PublicationPartOpt pub_partopt);
|
||||
extern List *GetExcludedPublicationTables(Oid pubid,
|
||||
PublicationPartOpt pub_partopt);
|
||||
extern List *GetAllTablesPublications(void);
|
||||
extern List *GetAllPublicationRelations(char relkind, bool pubviaroot);
|
||||
extern List *GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot);
|
||||
extern List *GetPublicationSchemas(Oid pubid);
|
||||
extern List *GetSchemaPublications(Oid schemaid);
|
||||
extern List *GetSchemaPublicationRelations(Oid schemaid,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId)
|
|||
Oid oid; /* oid */
|
||||
Oid prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */
|
||||
Oid prrelid BKI_LOOKUP(pg_class); /* Oid of the relation */
|
||||
bool prexcept BKI_DEFAULT(f); /* relation is not published */
|
||||
|
||||
#ifdef CATALOG_VARLEN /* variable-length fields start here */
|
||||
pg_node_tree prqual; /* qualifications */
|
||||
|
|
|
|||
|
|
@ -4299,9 +4299,10 @@ typedef struct AlterTSConfigurationStmt
|
|||
typedef struct PublicationTable
|
||||
{
|
||||
NodeTag type;
|
||||
RangeVar *relation; /* relation to be published */
|
||||
RangeVar *relation; /* publication relation */
|
||||
Node *whereClause; /* qualifications */
|
||||
List *columns; /* List of columns in a publication table */
|
||||
bool except; /* True if listed in the EXCEPT clause */
|
||||
} PublicationTable;
|
||||
|
||||
/*
|
||||
|
|
@ -4310,6 +4311,7 @@ typedef struct PublicationTable
|
|||
typedef enum PublicationObjSpecType
|
||||
{
|
||||
PUBLICATIONOBJ_TABLE, /* A table */
|
||||
PUBLICATIONOBJ_EXCEPT_TABLE, /* A table in the EXCEPT clause */
|
||||
PUBLICATIONOBJ_TABLES_IN_SCHEMA, /* All tables in schema */
|
||||
PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA, /* All tables in first element of
|
||||
* search_path */
|
||||
|
|
@ -4338,6 +4340,7 @@ typedef struct PublicationAllObjSpec
|
|||
{
|
||||
NodeTag type;
|
||||
PublicationAllObjType pubobjtype; /* type of this publication object */
|
||||
List *except_tables; /* tables specified in the EXCEPT clause */
|
||||
ParseLoc location; /* token location, or -1 if unknown */
|
||||
} PublicationAllObjSpec;
|
||||
|
||||
|
|
|
|||
|
|
@ -213,33 +213,157 @@ Not-null constraints:
|
|||
regress_publication_user | t | f | t | t | f | f | none | f |
|
||||
(1 row)
|
||||
|
||||
DROP TABLE testpub_tbl2;
|
||||
DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
|
||||
CREATE TABLE testpub_tbl3 (a int);
|
||||
CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
|
||||
---------------------------------------------
|
||||
-- EXCEPT TABLE tests for normal tables
|
||||
---------------------------------------------
|
||||
SET client_min_messages = 'ERROR';
|
||||
CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
|
||||
CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
|
||||
-- Specify table list in the EXCEPT TABLE clause of a FOR ALL TABLES publication
|
||||
CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
|
||||
\dRp+ testpub_foralltables_excepttable
|
||||
Publication testpub_foralltables_excepttable
|
||||
Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description
|
||||
--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+-------------
|
||||
regress_publication_user | t | f | t | t | t | t | none | f |
|
||||
Except tables:
|
||||
"public.testpub_tbl1"
|
||||
"public.testpub_tbl2"
|
||||
|
||||
-- Specify table in the EXCEPT TABLE clause of a FOR ALL TABLES publication
|
||||
CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT TABLE (testpub_tbl1);
|
||||
\dRp+ testpub_foralltables_excepttable1
|
||||
Publication testpub_foralltables_excepttable1
|
||||
Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description
|
||||
--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+-------------
|
||||
regress_publication_user | t | f | t | t | t | t | none | f |
|
||||
Except tables:
|
||||
"public.testpub_tbl1"
|
||||
|
||||
-- Check that the table description shows the publications where it is listed
|
||||
-- in the EXCEPT TABLE clause
|
||||
\d testpub_tbl1
|
||||
Table "public.testpub_tbl1"
|
||||
Column | Type | Collation | Nullable | Default
|
||||
--------+---------+-----------+----------+------------------------------------------
|
||||
id | integer | | not null | nextval('testpub_tbl1_id_seq'::regclass)
|
||||
data | text | | |
|
||||
Indexes:
|
||||
"testpub_tbl1_pkey" PRIMARY KEY, btree (id)
|
||||
Publications:
|
||||
"testpub_foralltables"
|
||||
Except Publications:
|
||||
"testpub_foralltables_excepttable"
|
||||
"testpub_foralltables_excepttable1"
|
||||
|
||||
RESET client_min_messages;
|
||||
DROP TABLE testpub_tbl2;
|
||||
DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
|
||||
---------------------------------------------
|
||||
-- Tests for inherited tables, and
|
||||
-- EXCEPT TABLE tests for inherited tables
|
||||
---------------------------------------------
|
||||
SET client_min_messages = 'ERROR';
|
||||
CREATE TABLE testpub_tbl_parent (a int);
|
||||
CREATE TABLE testpub_tbl_child (b text) INHERITS (testpub_tbl_parent);
|
||||
CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl_parent;
|
||||
\dRp+ testpub3
|
||||
Publication testpub3
|
||||
Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description
|
||||
--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+-------------
|
||||
regress_publication_user | f | f | t | t | t | t | none | f |
|
||||
Tables:
|
||||
"public.testpub_tbl3"
|
||||
"public.testpub_tbl3a"
|
||||
"public.testpub_tbl_child"
|
||||
"public.testpub_tbl_parent"
|
||||
|
||||
CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl_parent;
|
||||
\dRp+ testpub4
|
||||
Publication testpub4
|
||||
Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description
|
||||
--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+-------------
|
||||
regress_publication_user | f | f | t | t | t | t | none | f |
|
||||
Tables:
|
||||
"public.testpub_tbl3"
|
||||
"public.testpub_tbl_parent"
|
||||
|
||||
DROP TABLE testpub_tbl3, testpub_tbl3a;
|
||||
DROP PUBLICATION testpub3, testpub4;
|
||||
-- List the parent table in the EXCEPT TABLE clause (without ONLY or '*')
|
||||
CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent);
|
||||
\dRp+ testpub5
|
||||
Publication testpub5
|
||||
Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description
|
||||
--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+-------------
|
||||
regress_publication_user | t | f | t | t | t | t | none | f |
|
||||
Except tables:
|
||||
"public.testpub_tbl_child"
|
||||
"public.testpub_tbl_parent"
|
||||
|
||||
-- EXCEPT with '*': list the table and all its descendants in the EXCEPT TABLE clause
|
||||
CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent *);
|
||||
\dRp+ testpub6
|
||||
Publication testpub6
|
||||
Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description
|
||||
--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+-------------
|
||||
regress_publication_user | t | f | t | t | t | t | none | f |
|
||||
Except tables:
|
||||
"public.testpub_tbl_child"
|
||||
"public.testpub_tbl_parent"
|
||||
|
||||
-- EXCEPT with ONLY: list the table in the EXCEPT TABLE clause, but not its descendants
|
||||
CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl_parent);
|
||||
\dRp+ testpub7
|
||||
Publication testpub7
|
||||
Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description
|
||||
--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+-------------
|
||||
regress_publication_user | t | f | t | t | t | t | none | f |
|
||||
Except tables:
|
||||
"public.testpub_tbl_parent"
|
||||
|
||||
RESET client_min_messages;
|
||||
DROP TABLE testpub_tbl_parent, testpub_tbl_child;
|
||||
DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7;
|
||||
---------------------------------------------
|
||||
-- EXCEPT TABLE tests for partitioned tables
|
||||
---------------------------------------------
|
||||
SET client_min_messages = 'ERROR';
|
||||
CREATE TABLE testpub_root(a int) PARTITION BY RANGE(a);
|
||||
CREATE TABLE testpub_part1 PARTITION OF testpub_root FOR VALUES FROM (0) TO (100);
|
||||
CREATE PUBLICATION testpub8 FOR ALL TABLES EXCEPT TABLE (testpub_root);
|
||||
\dRp+ testpub8;
|
||||
Publication testpub8
|
||||
Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description
|
||||
--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+-------------
|
||||
regress_publication_user | t | f | t | t | t | t | none | f |
|
||||
Except tables:
|
||||
"public.testpub_root"
|
||||
|
||||
\d testpub_part1
|
||||
Table "public.testpub_part1"
|
||||
Column | Type | Collation | Nullable | Default
|
||||
--------+---------+-----------+----------+---------
|
||||
a | integer | | |
|
||||
Partition of: testpub_root FOR VALUES FROM (0) TO (100)
|
||||
Except Publications:
|
||||
"testpub8"
|
||||
|
||||
\d testpub_root
|
||||
Partitioned table "public.testpub_root"
|
||||
Column | Type | Collation | Nullable | Default
|
||||
--------+---------+-----------+----------+---------
|
||||
a | integer | | |
|
||||
Partition key: RANGE (a)
|
||||
Except Publications:
|
||||
"testpub8"
|
||||
Number of partitions: 1 (Use \d+ to list them.)
|
||||
|
||||
CREATE PUBLICATION testpub9 FOR ALL TABLES EXCEPT TABLE (testpub_part1);
|
||||
ERROR: cannot use publication EXCEPT clause for relation "testpub_part1"
|
||||
DETAIL: This operation is not supported for individual partitions.
|
||||
CREATE TABLE tab_main (a int) PARTITION BY RANGE(a);
|
||||
-- Attaching a partition is not allowed if the partitioned table appears in a
|
||||
-- publication's EXCEPT TABLE clause.
|
||||
ALTER TABLE tab_main ATTACH PARTITION testpub_root FOR VALUES FROM (0) TO (200);
|
||||
ERROR: cannot attach table "testpub_root" as partition because it is referenced in publication "testpub8" EXCEPT clause
|
||||
DETAIL: The publication EXCEPT clause cannot contain tables that are partitions.
|
||||
RESET client_min_messages;
|
||||
DROP TABLE testpub_root, testpub_part1, tab_main;
|
||||
DROP PUBLICATION testpub8;
|
||||
--- Tests for publications with SEQUENCES
|
||||
CREATE SEQUENCE regress_pub_seq0;
|
||||
CREATE SEQUENCE pub_test.regress_pub_seq1;
|
||||
|
|
|
|||
|
|
@ -105,20 +105,69 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall
|
|||
\d+ testpub_tbl2
|
||||
\dRp+ testpub_foralltables
|
||||
|
||||
DROP TABLE testpub_tbl2;
|
||||
DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema;
|
||||
|
||||
CREATE TABLE testpub_tbl3 (a int);
|
||||
CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
|
||||
---------------------------------------------
|
||||
-- EXCEPT TABLE tests for normal tables
|
||||
---------------------------------------------
|
||||
SET client_min_messages = 'ERROR';
|
||||
CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
|
||||
CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
|
||||
RESET client_min_messages;
|
||||
\dRp+ testpub3
|
||||
\dRp+ testpub4
|
||||
-- Specify table list in the EXCEPT TABLE clause of a FOR ALL TABLES publication
|
||||
CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT TABLE (testpub_tbl1, testpub_tbl2);
|
||||
\dRp+ testpub_foralltables_excepttable
|
||||
-- Specify table in the EXCEPT TABLE clause of a FOR ALL TABLES publication
|
||||
CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT TABLE (testpub_tbl1);
|
||||
\dRp+ testpub_foralltables_excepttable1
|
||||
-- Check that the table description shows the publications where it is listed
|
||||
-- in the EXCEPT TABLE clause
|
||||
\d testpub_tbl1
|
||||
|
||||
DROP TABLE testpub_tbl3, testpub_tbl3a;
|
||||
DROP PUBLICATION testpub3, testpub4;
|
||||
RESET client_min_messages;
|
||||
DROP TABLE testpub_tbl2;
|
||||
DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema, testpub_foralltables_excepttable, testpub_foralltables_excepttable1;
|
||||
|
||||
---------------------------------------------
|
||||
-- Tests for inherited tables, and
|
||||
-- EXCEPT TABLE tests for inherited tables
|
||||
---------------------------------------------
|
||||
SET client_min_messages = 'ERROR';
|
||||
CREATE TABLE testpub_tbl_parent (a int);
|
||||
CREATE TABLE testpub_tbl_child (b text) INHERITS (testpub_tbl_parent);
|
||||
CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl_parent;
|
||||
\dRp+ testpub3
|
||||
CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl_parent;
|
||||
\dRp+ testpub4
|
||||
-- List the parent table in the EXCEPT TABLE clause (without ONLY or '*')
|
||||
CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent);
|
||||
\dRp+ testpub5
|
||||
-- EXCEPT with '*': list the table and all its descendants in the EXCEPT TABLE clause
|
||||
CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT TABLE (testpub_tbl_parent *);
|
||||
\dRp+ testpub6
|
||||
-- EXCEPT with ONLY: list the table in the EXCEPT TABLE clause, but not its descendants
|
||||
CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT TABLE (ONLY testpub_tbl_parent);
|
||||
\dRp+ testpub7
|
||||
|
||||
RESET client_min_messages;
|
||||
DROP TABLE testpub_tbl_parent, testpub_tbl_child;
|
||||
DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7;
|
||||
|
||||
---------------------------------------------
|
||||
-- EXCEPT TABLE tests for partitioned tables
|
||||
---------------------------------------------
|
||||
SET client_min_messages = 'ERROR';
|
||||
CREATE TABLE testpub_root(a int) PARTITION BY RANGE(a);
|
||||
CREATE TABLE testpub_part1 PARTITION OF testpub_root FOR VALUES FROM (0) TO (100);
|
||||
CREATE PUBLICATION testpub8 FOR ALL TABLES EXCEPT TABLE (testpub_root);
|
||||
\dRp+ testpub8;
|
||||
\d testpub_part1
|
||||
\d testpub_root
|
||||
CREATE PUBLICATION testpub9 FOR ALL TABLES EXCEPT TABLE (testpub_part1);
|
||||
|
||||
CREATE TABLE tab_main (a int) PARTITION BY RANGE(a);
|
||||
-- Attaching a partition is not allowed if the partitioned table appears in a
|
||||
-- publication's EXCEPT TABLE clause.
|
||||
ALTER TABLE tab_main ATTACH PARTITION testpub_root FOR VALUES FROM (0) TO (200);
|
||||
|
||||
RESET client_min_messages;
|
||||
DROP TABLE testpub_root, testpub_part1, tab_main;
|
||||
DROP PUBLICATION testpub8;
|
||||
|
||||
--- Tests for publications with SEQUENCES
|
||||
CREATE SEQUENCE regress_pub_seq0;
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ tests += {
|
|||
't/034_temporal.pl',
|
||||
't/035_conflicts.pl',
|
||||
't/036_sequences.pl',
|
||||
't/037_except.pl',
|
||||
't/100_bugs.pl',
|
||||
],
|
||||
},
|
||||
|
|
|
|||
255
src/test/subscription/t/037_except.pl
Normal file
255
src/test/subscription/t/037_except.pl
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
|
||||
# Copyright (c) 2026, PostgreSQL Global Development Group
|
||||
|
||||
# Logical replication tests for EXCEPT TABLE publications
|
||||
use strict;
|
||||
use warnings;
|
||||
use PostgreSQL::Test::Cluster;
|
||||
use PostgreSQL::Test::Utils;
|
||||
use Test::More;
|
||||
|
||||
# Initialize publisher node
|
||||
my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
|
||||
$node_publisher->init(allows_streaming => 'logical');
|
||||
$node_publisher->start;
|
||||
|
||||
my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
|
||||
|
||||
# Initialize subscriber node
|
||||
my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
|
||||
$node_subscriber->init;
|
||||
$node_subscriber->start;
|
||||
|
||||
my $result;
|
||||
|
||||
sub test_except_root_partition
|
||||
{
|
||||
my ($pubviaroot) = @_;
|
||||
|
||||
# If the root partitioned table is in the EXCEPT TABLE clause, all its
|
||||
# partitions are excluded from publication, regardless of the
|
||||
# publish_via_partition_root setting.
|
||||
$node_publisher->safe_psql(
|
||||
'postgres', qq(
|
||||
CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT TABLE (root1) WITH (publish_via_partition_root = $pubviaroot);
|
||||
INSERT INTO root1 VALUES (1), (101);
|
||||
));
|
||||
$node_subscriber->safe_psql('postgres',
|
||||
"CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part"
|
||||
);
|
||||
$node_subscriber->wait_for_subscription_sync($node_publisher,
|
||||
'tap_sub_part');
|
||||
|
||||
# Advance the replication slot to ignore changes generated before this point.
|
||||
$node_publisher->safe_psql('postgres',
|
||||
"SELECT slot_name FROM pg_replication_slot_advance('test_slot', pg_current_wal_lsn())"
|
||||
);
|
||||
$node_publisher->safe_psql('postgres',
|
||||
"INSERT INTO root1 VALUES (2), (102)");
|
||||
|
||||
# Verify that data inserted into the partitioned table is not published when
|
||||
# it is in the EXCEPT TABLE clause.
|
||||
$result = $node_publisher->safe_psql('postgres',
|
||||
"SELECT count(*) = 0 FROM pg_logical_slot_get_binary_changes('test_slot', NULL, NULL, 'proto_version', '1', 'publication_names', 'tap_pub_part')"
|
||||
);
|
||||
$node_publisher->wait_for_catchup('tap_sub_part');
|
||||
|
||||
# Verify that no rows are replicated to subscriber for root or partitions.
|
||||
foreach my $table (qw(root1 part1 part2 part2_1))
|
||||
{
|
||||
$result = $node_subscriber->safe_psql('postgres',
|
||||
"SELECT count(*) FROM $table");
|
||||
is($result, qq(0), "no rows replicated to subscriber for $table");
|
||||
}
|
||||
|
||||
$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part");
|
||||
$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part");
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# EXCEPT TABLE test cases for non-partitioned tables and inherited tables.
|
||||
# ============================================
|
||||
|
||||
# Create schemas and tables on publisher
|
||||
$node_publisher->safe_psql(
|
||||
'postgres', qq(
|
||||
CREATE TABLE tab1 AS SELECT generate_series(1,10) AS a;
|
||||
CREATE TABLE parent (a int);
|
||||
CREATE TABLE child (b int) INHERITS (parent);
|
||||
CREATE TABLE parent1 (a int);
|
||||
CREATE TABLE child1 (b int) INHERITS (parent1);
|
||||
));
|
||||
|
||||
# Create schemas and tables on subscriber
|
||||
$node_subscriber->safe_psql(
|
||||
'postgres', qq(
|
||||
CREATE TABLE tab1 (a int);
|
||||
CREATE TABLE parent (a int);
|
||||
CREATE TABLE child (b int) INHERITS (parent);
|
||||
CREATE TABLE parent1 (a int);
|
||||
CREATE TABLE child1 (b int) INHERITS (parent1);
|
||||
));
|
||||
|
||||
# Exclude tab1 (non-inheritance case), and also exclude parent and ONLY parent1
|
||||
# to verify exclusion behavior for inherited tables, including the effect of
|
||||
# ONLY in the EXCEPT TABLE clause.
|
||||
$node_publisher->safe_psql('postgres',
|
||||
"CREATE PUBLICATION tab_pub FOR ALL TABLES EXCEPT TABLE (tab1, parent, only parent1)"
|
||||
);
|
||||
|
||||
# Create a logical replication slot to help with later tests.
|
||||
$node_publisher->safe_psql('postgres',
|
||||
"SELECT pg_create_logical_replication_slot('test_slot', 'pgoutput')");
|
||||
|
||||
$node_subscriber->safe_psql('postgres',
|
||||
"CREATE SUBSCRIPTION tab_sub CONNECTION '$publisher_connstr' PUBLICATION tab_pub"
|
||||
);
|
||||
|
||||
# Wait for initial table sync to finish
|
||||
$node_subscriber->wait_for_subscription_sync($node_publisher, 'tab_sub');
|
||||
|
||||
# Check the table data does not sync for the tables specified in EXCEPT TABLE
|
||||
# clause.
|
||||
$result =
|
||||
$node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab1");
|
||||
is($result, qq(0),
|
||||
'check there is no initial data copied for the tables specified in the EXCEPT TABLE clause'
|
||||
);
|
||||
|
||||
# Insert some data into the table listed in the EXCEPT TABLE clause
|
||||
$node_publisher->safe_psql(
|
||||
'postgres', qq(
|
||||
INSERT INTO tab1 VALUES(generate_series(11,20));
|
||||
INSERT INTO child VALUES(generate_series(11,20), generate_series(11,20));
|
||||
));
|
||||
|
||||
# Verify that data inserted into a table listed in the EXCEPT TABLE clause is
|
||||
# not published.
|
||||
$result = $node_publisher->safe_psql('postgres',
|
||||
"SELECT count(*) = 0 FROM pg_logical_slot_get_binary_changes('test_slot', NULL, NULL, 'proto_version', '1', 'publication_names', 'tab_pub')"
|
||||
);
|
||||
is($result, qq(t),
|
||||
'verify no changes for table listed in the EXCEPT TABLE clause are present in the replication slot'
|
||||
);
|
||||
|
||||
# This should be published because ONLY parent1 was specified in the
|
||||
# EXCEPT TABLE clause, so the exclusion applies only to the parent table and not
|
||||
# to its child.
|
||||
$node_publisher->safe_psql('postgres',
|
||||
"INSERT INTO child1 VALUES(generate_series(11,20), generate_series(11,20))"
|
||||
);
|
||||
|
||||
# Verify that data inserted into a table listed in the EXCEPT TABLE clause is
|
||||
# not replicated.
|
||||
$node_publisher->wait_for_catchup('tab_sub');
|
||||
$result =
|
||||
$node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab1");
|
||||
is($result, qq(0), 'check replicated inserts on subscriber');
|
||||
$result =
|
||||
$node_subscriber->safe_psql('postgres', "SELECT count(*) FROM child");
|
||||
is($result, qq(0), 'check replicated inserts on subscriber');
|
||||
$result =
|
||||
$node_subscriber->safe_psql('postgres', "SELECT count(*) FROM child1");
|
||||
is($result, qq(10), 'check replicated inserts on subscriber');
|
||||
|
||||
# cleanup
|
||||
$node_subscriber->safe_psql(
|
||||
'postgres', qq(
|
||||
DROP SUBSCRIPTION tab_sub;
|
||||
TRUNCATE TABLE tab1;
|
||||
DROP TABLE parent, parent1, child, child1;
|
||||
));
|
||||
$node_publisher->safe_psql(
|
||||
'postgres', qq(
|
||||
DROP PUBLICATION tab_pub;
|
||||
TRUNCATE TABLE tab1;
|
||||
DROP TABLE parent, parent1, child, child1;
|
||||
));
|
||||
|
||||
# ============================================
|
||||
# EXCEPT TABLE test cases for partitioned tables
|
||||
# ============================================
|
||||
# Setup partitioned table and partitions on the publisher that map to normal
|
||||
# tables on the subscriber.
|
||||
$node_publisher->safe_psql(
|
||||
'postgres', qq(
|
||||
CREATE TABLE root1(a int) PARTITION BY RANGE(a);
|
||||
CREATE TABLE part1 PARTITION OF root1 FOR VALUES FROM (0) TO (100);
|
||||
CREATE TABLE part2 PARTITION OF root1 FOR VALUES FROM (100) TO (200) PARTITION BY RANGE(a);
|
||||
CREATE TABLE part2_1 PARTITION OF part2 FOR VALUES FROM (100) TO (150);
|
||||
));
|
||||
|
||||
$node_subscriber->safe_psql(
|
||||
'postgres', qq(
|
||||
CREATE TABLE root1(a int);
|
||||
CREATE TABLE part1(a int);
|
||||
CREATE TABLE part2(a int);
|
||||
CREATE TABLE part2_1(a int);
|
||||
));
|
||||
|
||||
# Validate the behaviour with both publish_via_partition_root as true and false
|
||||
test_except_root_partition('false');
|
||||
test_except_root_partition('true');
|
||||
|
||||
# ============================================
|
||||
# Test when a subscription is subscribing to multiple publications
|
||||
# ============================================
|
||||
|
||||
# OK when a table is excluded by pub1 EXCEPT TABLE, but it is included by pub2
|
||||
# FOR TABLE.
|
||||
$node_publisher->safe_psql(
|
||||
'postgres', qq(
|
||||
CREATE PUBLICATION tap_pub1 FOR ALL TABLES EXCEPT TABLE (tab1);
|
||||
CREATE PUBLICATION tap_pub2 FOR TABLE tab1;
|
||||
INSERT INTO tab1 VALUES(1);
|
||||
));
|
||||
$node_subscriber->psql('postgres',
|
||||
"CREATE SUBSCRIPTION tap_sub CONNECTION '$publisher_connstr' PUBLICATION tap_pub1, tap_pub2"
|
||||
);
|
||||
$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub');
|
||||
|
||||
$node_publisher->safe_psql('postgres', qq(INSERT INTO tab1 VALUES(2)));
|
||||
$node_publisher->wait_for_catchup('tap_sub');
|
||||
|
||||
$result =
|
||||
$node_publisher->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a");
|
||||
is( $result, qq(1
|
||||
2),
|
||||
"check replication of a table in the EXCEPT TABLE clause of one publication but included by another"
|
||||
);
|
||||
$node_publisher->safe_psql(
|
||||
'postgres', qq(
|
||||
DROP PUBLICATION tap_pub2;
|
||||
TRUNCATE tab1;
|
||||
));
|
||||
$node_subscriber->safe_psql('postgres', qq(TRUNCATE tab1));
|
||||
|
||||
# OK when a table is excluded by pub1 EXCEPT TABLE, but it is included by pub2
|
||||
# FOR ALL TABLES.
|
||||
$node_publisher->safe_psql(
|
||||
'postgres', qq(
|
||||
CREATE PUBLICATION tap_pub2 FOR ALL TABLES;
|
||||
INSERT INTO tab1 VALUES(1);
|
||||
));
|
||||
$node_subscriber->psql('postgres',
|
||||
"CREATE SUBSCRIPTION tap_sub CONNECTION '$publisher_connstr' PUBLICATION tap_pub1, tap_pub2"
|
||||
);
|
||||
$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub');
|
||||
|
||||
$node_publisher->safe_psql('postgres', qq(INSERT INTO tab1 VALUES(2)));
|
||||
$node_publisher->wait_for_catchup('tap_sub');
|
||||
|
||||
$result =
|
||||
$node_publisher->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a");
|
||||
is( $result, qq(1
|
||||
2),
|
||||
"check replication of a table in the EXCEPT TABLE clause of one publication but included by another"
|
||||
);
|
||||
|
||||
$node_subscriber->safe_psql('postgres', 'DROP SUBSCRIPTION tap_sub');
|
||||
$node_publisher->safe_psql('postgres', 'DROP PUBLICATION tap_pub1');
|
||||
$node_publisher->safe_psql('postgres', 'DROP PUBLICATION tap_pub2');
|
||||
|
||||
$node_publisher->stop('fast');
|
||||
|
||||
done_testing();
|
||||
Loading…
Reference in a new issue