Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 87ce27d

Browse files
author
Amit Kapila
committed
Ensure stored generated columns must be published when required.
Ensure stored generated columns that are part of REPLICA IDENTITY must be published explicitly for UPDATE and DELETE operations to be published. We can publish generated columns by listing them in the column list or by enabling the publish_generated_columns option. This commit changes the behavior of the test added in commit adedf54 by giving an ERROR for the UPDATE operation in such cases. There is no way to trigger the bug reported in commit adedf54 but we didn't remove the corresponding code change because it is still relevant when replicating changes from a publisher with version less than 18. We decided not to backpatch this behavior change to avoid the risk of breaking existing output plugins that may be sending generated columns by default although we are not aware of any such plugin. Also, we didn't see any reports related to this on STABLE branches which is another reason not to backpatch this change. Author: Shlok Kyal, Hou Zhijie Reviewed-by: Vignesh C, Amit Kapila Discussion: https://postgr.es/m/CANhcyEVw4V2Awe2AB6i0E5AJLNdASShGfdBLbUd1XtWDboymCA@mail.gmail.com
1 parent 77c189c commit 87ce27d

File tree

9 files changed

+243
-100
lines changed

9 files changed

+243
-100
lines changed

doc/src/sgml/ref/create_publication.sgml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,14 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
311311
system columns.
312312
</para>
313313

314+
<para>
315+
The generated columns that are part of <literal>REPLICA IDENTITY</literal>
316+
must be published explicitly either by listing them in the column list or
317+
by enabling the <literal>publish_generated_columns</literal> option, in
318+
order for <command>UPDATE</command> and <command>DELETE</command> operations
319+
to be published.
320+
</para>
321+
314322
<para>
315323
The row filter on a table becomes redundant if
316324
<literal>FOR TABLES IN SCHEMA</literal> is specified and the table

src/backend/commands/publicationcmds.c

Lines changed: 91 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -336,21 +336,36 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
336336
}
337337

338338
/*
339-
* Check if all columns referenced in the REPLICA IDENTITY are covered by
340-
* the column list.
339+
* Check for invalid columns in the publication table definition.
341340
*
342-
* Returns true if any replica identity column is not covered by column list.
341+
* This function evaluates two conditions:
342+
*
343+
* 1. Ensures that all columns referenced in the REPLICA IDENTITY are covered
344+
* by the column list. If any column is missing, *invalid_column_list is set
345+
* to true.
346+
* 2. Ensures that all the generated columns referenced in the REPLICA IDENTITY
347+
* are published either by listing them in the column list or by enabling
348+
* publish_generated_columns option. If any unpublished generated column is
349+
* found, *invalid_gen_col is set to true.
350+
*
351+
* Returns true if any of the above conditions are not met.
343352
*/
344353
bool
345-
pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
346-
bool pubviaroot)
354+
pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
355+
bool pubviaroot, bool pubgencols,
356+
bool *invalid_column_list,
357+
bool *invalid_gen_col)
347358
{
348-
HeapTuple tuple;
349359
Oid relid = RelationGetRelid(relation);
350360
Oid publish_as_relid = RelationGetRelid(relation);
351-
bool result = false;
352-
Datum datum;
353-
bool isnull;
361+
Bitmapset *idattrs;
362+
Bitmapset *columns = NULL;
363+
TupleDesc desc = RelationGetDescr(relation);
364+
Publication *pub;
365+
int x;
366+
367+
*invalid_column_list = false;
368+
*invalid_gen_col = false;
354369

355370
/*
356371
* For a partition, if pubviaroot is true, find the topmost ancestor that
@@ -368,80 +383,91 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
368383
publish_as_relid = relid;
369384
}
370385

371-
tuple = SearchSysCache2(PUBLICATIONRELMAP,
372-
ObjectIdGetDatum(publish_as_relid),
373-
ObjectIdGetDatum(pubid));
386+
/* Fetch the column list */
387+
pub = GetPublication(pubid);
388+
check_and_fetch_column_list(pub, publish_as_relid, NULL, &columns);
374389

375-
if (!HeapTupleIsValid(tuple))
376-
return false;
390+
if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
391+
{
392+
/* With REPLICA IDENTITY FULL, no column list is allowed. */
393+
*invalid_column_list = (columns != NULL);
377394

378-
datum = SysCacheGetAttr(PUBLICATIONRELMAP, tuple,
379-
Anum_pg_publication_rel_prattrs,
380-
&isnull);
395+
/*
396+
* As we don't allow a column list with REPLICA IDENTITY FULL, the
397+
* publish_generated_columns option must be set to true if the table
398+
* has any stored generated columns.
399+
*/
400+
if (!pubgencols &&
401+
relation->rd_att->constr &&
402+
relation->rd_att->constr->has_generated_stored)
403+
*invalid_gen_col = true;
381404

382-
if (!isnull)
383-
{
384-
int x;
385-
Bitmapset *idattrs;
386-
Bitmapset *columns = NULL;
405+
if (*invalid_gen_col && *invalid_column_list)
406+
return true;
407+
}
387408

388-
/* With REPLICA IDENTITY FULL, no column list is allowed. */
389-
if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL)
390-
result = true;
409+
/* Remember columns that are part of the REPLICA IDENTITY */
410+
idattrs = RelationGetIndexAttrBitmap(relation,
411+
INDEX_ATTR_BITMAP_IDENTITY_KEY);
391412

392-
/* Transform the column list datum to a bitmapset. */
393-
columns = pub_collist_to_bitmapset(NULL, datum, NULL);
413+
/*
414+
* Attnums in the bitmap returned by RelationGetIndexAttrBitmap are offset
415+
* (to handle system columns the usual way), while column list does not
416+
* use offset, so we can't do bms_is_subset(). Instead, we have to loop
417+
* over the idattrs and check all of them are in the list.
418+
*/
419+
x = -1;
420+
while ((x = bms_next_member(idattrs, x)) >= 0)
421+
{
422+
AttrNumber attnum = (x + FirstLowInvalidHeapAttributeNumber);
423+
Form_pg_attribute att = TupleDescAttr(desc, attnum - 1);
394424

395-
/* Remember columns that are part of the REPLICA IDENTITY */
396-
idattrs = RelationGetIndexAttrBitmap(relation,
397-
INDEX_ATTR_BITMAP_IDENTITY_KEY);
425+
if (columns == NULL)
426+
{
427+
/*
428+
* The publish_generated_columns option must be set to true if the
429+
* REPLICA IDENTITY contains any stored generated column.
430+
*/
431+
if (!pubgencols && att->attgenerated)
432+
{
433+
*invalid_gen_col = true;
434+
break;
435+
}
436+
437+
/* Skip validating the column list since it is not defined */
438+
continue;
439+
}
398440

399441
/*
400-
* Attnums in the bitmap returned by RelationGetIndexAttrBitmap are
401-
* offset (to handle system columns the usual way), while column list
402-
* does not use offset, so we can't do bms_is_subset(). Instead, we
403-
* have to loop over the idattrs and check all of them are in the
404-
* list.
442+
* If pubviaroot is true, we are validating the column list of the
443+
* parent table, but the bitmap contains the replica identity
444+
* information of the child table. The parent/child attnums may not
445+
* match, so translate them to the parent - get the attname from the
446+
* child, and look it up in the parent.
405447
*/
406-
x = -1;
407-
while ((x = bms_next_member(idattrs, x)) >= 0)
448+
if (pubviaroot)
408449
{
409-
AttrNumber attnum = (x + FirstLowInvalidHeapAttributeNumber);
450+
/* attribute name in the child table */
451+
char *colname = get_attname(relid, attnum, false);
410452

411453
/*
412-
* If pubviaroot is true, we are validating the column list of the
413-
* parent table, but the bitmap contains the replica identity
414-
* information of the child table. The parent/child attnums may
415-
* not match, so translate them to the parent - get the attname
416-
* from the child, and look it up in the parent.
454+
* Determine the attnum for the attribute name in parent (we are
455+
* using the column list defined on the parent).
417456
*/
418-
if (pubviaroot)
419-
{
420-
/* attribute name in the child table */
421-
char *colname = get_attname(relid, attnum, false);
422-
423-
/*
424-
* Determine the attnum for the attribute name in parent (we
425-
* are using the column list defined on the parent).
426-
*/
427-
attnum = get_attnum(publish_as_relid, colname);
428-
}
429-
430-
/* replica identity column, not covered by the column list */
431-
if (!bms_is_member(attnum, columns))
432-
{
433-
result = true;
434-
break;
435-
}
457+
attnum = get_attnum(publish_as_relid, colname);
436458
}
437459

438-
bms_free(idattrs);
439-
bms_free(columns);
460+
/* replica identity column, not covered by the column list */
461+
*invalid_column_list |= !bms_is_member(attnum, columns);
462+
463+
if (*invalid_column_list && *invalid_gen_col)
464+
break;
440465
}
441466

442-
ReleaseSysCache(tuple);
467+
bms_free(columns);
468+
bms_free(idattrs);
443469

444-
return result;
470+
return *invalid_column_list || *invalid_gen_col;
445471
}
446472

447473
/* check_functions_in_node callback */

src/backend/executor/execReplication.c

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -785,16 +785,27 @@ CheckCmdReplicaIdentity(Relation rel, CmdType cmd)
785785
return;
786786

787787
/*
788-
* It is only safe to execute UPDATE/DELETE when all columns, referenced
789-
* in the row filters from publications which the relation is in, are
790-
* valid - i.e. when all referenced columns are part of REPLICA IDENTITY
791-
* or the table does not publish UPDATEs or DELETEs.
788+
* It is only safe to execute UPDATE/DELETE if the relation does not
789+
* publish UPDATEs or DELETEs, or all the following conditions are
790+
* satisfied:
791+
*
792+
* 1. All columns, referenced in the row filters from publications which
793+
* the relation is in, are valid - i.e. when all referenced columns are
794+
* part of REPLICA IDENTITY.
795+
*
796+
* 2. All columns, referenced in the column lists are valid - i.e. when
797+
* all columns referenced in the REPLICA IDENTITY are covered by the
798+
* column list.
799+
*
800+
* 3. All generated columns in REPLICA IDENTITY of the relation, are valid
801+
* - i.e. when all these generated columns are published.
792802
*
793803
* XXX We could optimize it by first checking whether any of the
794-
* publications have a row filter for this relation. If not and relation
795-
* has replica identity then we can avoid building the descriptor but as
796-
* this happens only one time it doesn't seem worth the additional
797-
* complexity.
804+
* publications have a row filter or column list for this relation, or if
805+
* the relation contains a generated column. If none of these exist and
806+
* the relation has replica identity then we can avoid building the
807+
* descriptor but as this happens only one time it doesn't seem worth the
808+
* additional complexity.
798809
*/
799810
RelationBuildPublicationDesc(rel, &pubdesc);
800811
if (cmd == CMD_UPDATE && !pubdesc.rf_valid_for_update)
@@ -809,6 +820,12 @@ CheckCmdReplicaIdentity(Relation rel, CmdType cmd)
809820
errmsg("cannot update table \"%s\"",
810821
RelationGetRelationName(rel)),
811822
errdetail("Column list used by the publication does not cover the replica identity.")));
823+
else if (cmd == CMD_UPDATE && !pubdesc.gencols_valid_for_update)
824+
ereport(ERROR,
825+
(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
826+
errmsg("cannot update table \"%s\"",
827+
RelationGetRelationName(rel)),
828+
errdetail("Replica identity consists of an unpublished generated column.")));
812829
else if (cmd == CMD_DELETE && !pubdesc.rf_valid_for_delete)
813830
ereport(ERROR,
814831
(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
@@ -821,6 +838,12 @@ CheckCmdReplicaIdentity(Relation rel, CmdType cmd)
821838
errmsg("cannot delete from table \"%s\"",
822839
RelationGetRelationName(rel)),
823840
errdetail("Column list used by the publication does not cover the replica identity.")));
841+
else if (cmd == CMD_DELETE && !pubdesc.gencols_valid_for_delete)
842+
ereport(ERROR,
843+
(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
844+
errmsg("cannot delete from table \"%s\"",
845+
RelationGetRelationName(rel)),
846+
errdetail("Replica identity consists of an unpublished generated column.")));
824847

825848
/* If relation has replica identity we are always good. */
826849
if (OidIsValid(RelationGetReplicaIndex(rel)))

src/backend/utils/cache/relcache.c

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5706,12 +5706,19 @@ RelationGetExclusionInfo(Relation indexRelation,
57065706
* Get the publication information for the given relation.
57075707
*
57085708
* Traverse all the publications which the relation is in to get the
5709-
* publication actions and validate the row filter expressions for such
5710-
* publications if any. We consider the row filter expression as invalid if it
5711-
* references any column which is not part of REPLICA IDENTITY.
5709+
* publication actions and validate:
5710+
* 1. The row filter expressions for such publications if any. We consider the
5711+
* row filter expression as invalid if it references any column which is not
5712+
* part of REPLICA IDENTITY.
5713+
* 2. The column list for such publication if any. We consider the column list
5714+
* invalid if REPLICA IDENTITY contains any column that is not part of it.
5715+
* 3. The generated columns of the relation for such publications. We consider
5716+
* any reference of an unpublished generated column in REPLICA IDENTITY as
5717+
* invalid.
57125718
*
57135719
* To avoid fetching the publication information repeatedly, we cache the
5714-
* publication actions and row filter validation information.
5720+
* publication actions, row filter validation information, column list
5721+
* validation information, and generated column validation information.
57155722
*/
57165723
void
57175724
RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
@@ -5734,6 +5741,8 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
57345741
pubdesc->rf_valid_for_delete = true;
57355742
pubdesc->cols_valid_for_update = true;
57365743
pubdesc->cols_valid_for_delete = true;
5744+
pubdesc->gencols_valid_for_update = true;
5745+
pubdesc->gencols_valid_for_delete = true;
57375746
return;
57385747
}
57395748

@@ -5748,6 +5757,8 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
57485757
pubdesc->rf_valid_for_delete = true;
57495758
pubdesc->cols_valid_for_update = true;
57505759
pubdesc->cols_valid_for_delete = true;
5760+
pubdesc->gencols_valid_for_update = true;
5761+
pubdesc->gencols_valid_for_delete = true;
57515762

57525763
/* Fetch the publication membership info. */
57535764
puboids = GetRelationPublications(relid);
@@ -5777,6 +5788,8 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
57775788
Oid pubid = lfirst_oid(lc);
57785789
HeapTuple tup;
57795790
Form_pg_publication pubform;
5791+
bool invalid_column_list;
5792+
bool invalid_gen_col;
57805793

57815794
tup = SearchSysCache1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
57825795

@@ -5811,18 +5824,27 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
58115824
/*
58125825
* Check if all columns are part of the REPLICA IDENTITY index or not.
58135826
*
5814-
* If the publication is FOR ALL TABLES then it means the table has no
5815-
* column list and we can skip the validation.
5827+
* Check if all generated columns included in the REPLICA IDENTITY are
5828+
* published.
58165829
*/
5817-
if (!pubform->puballtables &&
5818-
(pubform->pubupdate || pubform->pubdelete) &&
5819-
pub_collist_contains_invalid_column(pubid, relation, ancestors,
5820-
pubform->pubviaroot))
5830+
if ((pubform->pubupdate || pubform->pubdelete) &&
5831+
pub_contains_invalid_column(pubid, relation, ancestors,
5832+
pubform->pubviaroot,
5833+
pubform->pubgencols,
5834+
&invalid_column_list,
5835+
&invalid_gen_col))
58215836
{
58225837
if (pubform->pubupdate)
5823-
pubdesc->cols_valid_for_update = false;
5838+
{
5839+
pubdesc->cols_valid_for_update = !invalid_column_list;
5840+
pubdesc->gencols_valid_for_update = !invalid_gen_col;
5841+
}
5842+
58245843
if (pubform->pubdelete)
5825-
pubdesc->cols_valid_for_delete = false;
5844+
{
5845+
pubdesc->cols_valid_for_delete = !invalid_column_list;
5846+
pubdesc->gencols_valid_for_delete = !invalid_gen_col;
5847+
}
58265848
}
58275849

58285850
ReleaseSysCache(tup);
@@ -5846,6 +5868,17 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
58465868
pubdesc->pubactions.pubdelete && pubdesc->pubactions.pubtruncate &&
58475869
!pubdesc->cols_valid_for_update && !pubdesc->cols_valid_for_delete)
58485870
break;
5871+
5872+
/*
5873+
* If we know everything is replicated and replica identity has an
5874+
* unpublished generated column, there is no point to check for other
5875+
* publications.
5876+
*/
5877+
if (pubdesc->pubactions.pubinsert && pubdesc->pubactions.pubupdate &&
5878+
pubdesc->pubactions.pubdelete && pubdesc->pubactions.pubtruncate &&
5879+
!pubdesc->gencols_valid_for_update &&
5880+
!pubdesc->gencols_valid_for_delete)
5881+
break;
58495882
}
58505883

58515884
if (relation->rd_pubdesc)

src/include/catalog/pg_publication.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ typedef struct PublicationDesc
9898
*/
9999
bool cols_valid_for_update;
100100
bool cols_valid_for_delete;
101+
102+
/*
103+
* true if all generated columns that are part of replica identity are
104+
* published or the publication actions do not include UPDATE or DELETE.
105+
*/
106+
bool gencols_valid_for_update;
107+
bool gencols_valid_for_delete;
101108
} PublicationDesc;
102109

103110
typedef struct Publication

0 commit comments

Comments
 (0)