From 0ce3ac739a0b6d8f8f6e3bdb8684b9e7f413c7df Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Mon, 3 Aug 2026 00:14:57 +0200 Subject: [PATCH v2 2/3] Peserve replica identity and publications in MERGE/SPLIT PARTITION(s) The new partition(s) created by ALTER TABLE ... MERGE/SPLIT PARTITION are built from the partitioned-table template, so they would default to REPLICA IDENTITY DEFAULT and silently drop out of any publication that the source partitions were directly part of, changing replication behavior without a warning. Carry a uniform, simply-representable replica identity (DEFAULT, FULL or NOTHING) from the source partitions to the new partition(s). Raise an error if the sources disagree, or use an index-based identity that cannot be reproduced automatically, and let the user set it explicitly. Also refuse the operation when any source partition is a direct member of a publication: the new partition would otherwise leave it, and faithfully reproducing per-relation column lists and row filters is ambiguous (especially when several sources are merged). Publications that cover the partitioned root continue to include the new partition, so those are unaffected. Document this behavior and add a test coverage. Discussion: https://postgr.es/m/CAN4CZFNCU=t09M=+r2t9hHLJuujdM4oQ8hCK_Sx-GpfiwMAicw@mail.gmail.com --- doc/src/sgml/ref/alter_table.sgml | 27 ++++++ src/backend/commands/tablecmds.c | 85 +++++++++++++++++++ src/test/regress/expected/partition_merge.out | 33 +++++++ src/test/regress/expected/partition_split.out | 28 ++++++ src/test/regress/sql/partition_merge.sql | 28 ++++++ src/test/regress/sql/partition_split.sql | 22 +++++ 6 files changed, 223 insertions(+) diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index aaf4dfd111a..c034745365c 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -1281,6 +1281,21 @@ WITH ( MODULUS numeric_literal, REM dependencies are not silently lost during merge. + + The new partition takes its replica identity from the merged partitions + when they all use the same simple setting + (DEFAULT, FULL or + NOTHING). If they use different settings, or use + REPLICA IDENTITY USING INDEX, the error is issued + and the command is aborted. Give the partitions being merged a uniform, + non-index replica identity before merging, and set a different replica + identity on the resulting partition afterwards if desired. Likewise, if + any of the partitions being merged is directly part of a publication, the + command is aborted; publish the partitioned table itself instead of the + individual partitions, or remove the partition from the publication before + merging. + + Moving rows into the new partition does not emit logical replication messages, in the same way that CLUSTER or @@ -1396,6 +1411,18 @@ WITH ( MODULUS numeric_literal, REM from the source partition's indexes. + + The new partitions take their replica identity from the split partition, + unless it uses REPLICA IDENTITY USING INDEX, in which + case the error is issued and the command is aborted. Give the partition + being split a non-index replica identity before splitting, and set a + different replica identity on the new partitions afterwards if desired. + Likewise, if the partition being split is directly part of a publication, + the command is rejected; publish the partitioned table itself instead of + the individual partitions, or remove the partition from the publication + before splitting. + + Moving rows into the new partitions does not emit logical replication messages, in the same way that CLUSTER or diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 0eb85c1be17..5fd6173b533 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -49,6 +49,7 @@ #include "catalog/pg_opclass.h" #include "catalog/pg_policy.h" #include "catalog/pg_proc.h" +#include "catalog/pg_publication.h" #include "catalog/pg_publication_rel.h" #include "catalog/pg_rewrite.h" #include "catalog/pg_statistic_ext.h" @@ -23353,6 +23354,78 @@ createPartitionTable(List **wqueue, RangeVar *newPartName, return newRel; } +/* + * transferPartitionReplicaIdentity: propagate the source partitions' replica + * identity to the new partition(s) created by MERGE/SPLIT, and refuse the + * operation for cases we cannot handle without silently changing replication + * behavior. + * + * The new partitions are built from the partitioned-table template and would + * otherwise default to REPLICA IDENTITY DEFAULT and drop out of any publication + * that the source partitions were directly part of. To avoid silent surprises: + * + * - A uniform, simply-representable replica identity (DEFAULT, FULL or + * NOTHING) is carried over to every new partition. If the sources disagree, + * or use an index-based identity (which cannot be reproduced on the new + * partition automatically), we raise an error and ask the user to set it. + * + * - If any source partition is a direct member of a publication, we refuse the + * operation: the new partition would silently leave the publication, and + * faithfully reproducing per-relation column lists and row filters is + * ambiguous (especially when several sources are merged). Publications that + * cover the partitioned root instead continue to include the new partition. + * + * 'sourceOids' lists the source partition OIDs (still present, not yet dropped); + * 'newPartRels' lists the new partition Relations (exclusively locked). + */ +static void +transferPartitionReplicaIdentity(List *sourceOids, List *newPartRels) +{ + char ri_type = '\0'; + bool ri_seen = false; + + foreach_oid(srcOid, sourceOids) + { + Relation src = table_open(srcOid, NoLock); + + if (GetRelationIncludedPublications(srcOid) != NIL) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot merge or split partition \"%s\" that is directly part of a publication", + RelationGetRelationName(src)), + errhint("Publish the partitioned table instead, or add the new partition to the publication after the operation.")); + + if (!ri_seen) + { + ri_type = src->rd_rel->relreplident; + ri_seen = true; + } + else if (ri_type != src->rd_rel->relreplident) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("partitions being merged have different replica identity settings"), + errhint("Set the replica identity of the new partition explicitly after the operation.")); + + table_close(src, NoLock); + } + + /* Nothing to carry over, or the new partitions already match. */ + if (!ri_seen || ri_type == REPLICA_IDENTITY_DEFAULT) + return; + + if (ri_type == REPLICA_IDENTITY_INDEX) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot automatically transfer an index-based replica identity to the new partition"), + errhint("Set the replica identity of the new partition explicitly with ALTER TABLE ... REPLICA IDENTITY USING INDEX.")); + + /* Carry FULL / NOTHING over to each new partition. */ + foreach_ptr(RelationData, newrel, newPartRels) + relation_mark_replica_identity(newrel, ri_type, InvalidOid, true); + + CommandCounterIncrement(); +} + /* * MergePartitionsMoveRows: scan partitions to be merged (mergingPartitions) * of the partitioned table and move rows into the new partition @@ -23903,6 +23976,12 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel, Assert(OidIsValid(ownerId)); newPartRel = createPartitionTable(wqueue, cmd->name, rel, ownerId); + /* + * Carry the source partitions' replica identity over to the new partition, + * and reject cases that would silently change replication behavior. + */ + transferPartitionReplicaIdentity(mergingPartitions, list_make1(newPartRel)); + /* * Switch to the table owner's userid, so that any index functions are run * as that user. Also, lockdown security-restricted operations and @@ -24345,6 +24424,12 @@ ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel, newPartRels = lappend(newPartRels, newPartRel); } + /* + * Carry the split partition's replica identity over to the new partitions, + * and reject cases that would silently change replication behavior. + */ + transferPartitionReplicaIdentity(list_make1_oid(splitRelOid), newPartRels); + /* * Switch to the table owner's userid, so that any index functions are run * as that user. Also, lockdown security-restricted operations and diff --git a/src/test/regress/expected/partition_merge.out b/src/test/regress/expected/partition_merge.out index ccda2b5843b..75d06beae19 100644 --- a/src/test/regress/expected/partition_merge.out +++ b/src/test/regress/expected/partition_merge.out @@ -1167,6 +1167,39 @@ SELECT reltablespace FROM pg_class WHERE relname = 'tp_merged'; 0 (1 row) +DROP TABLE t; +-- MERGE PARTITIONS carries over a uniform replica identity ... +CREATE TABLE t (i int PRIMARY KEY) PARTITION BY RANGE (i); +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +ALTER TABLE tp_0_1 REPLICA IDENTITY FULL; +ALTER TABLE tp_1_2 REPLICA IDENTITY FULL; +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +SELECT relreplident FROM pg_class WHERE relname = 'tp_0_2'; + relreplident +-------------- + f +(1 row) + +DROP TABLE t; +-- ... but rejects merging partitions with different replica identities. +CREATE TABLE t (i int PRIMARY KEY) PARTITION BY RANGE (i); +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +ALTER TABLE tp_0_1 REPLICA IDENTITY FULL; +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; -- fails +ERROR: partitions being merged have different replica identity settings +HINT: Set the replica identity of the new partition explicitly after the operation. +DROP TABLE t; +-- MERGE PARTITIONS rejects a partition that is directly part of a publication. +CREATE TABLE t (i int PRIMARY KEY) PARTITION BY RANGE (i); +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +CREATE PUBLICATION pub_merge FOR TABLE tp_0_1; +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; -- fails +ERROR: cannot merge or split partition "tp_0_1" that is directly part of a publication +HINT: Publish the partitioned table instead, or add the new partition to the publication after the operation. +DROP PUBLICATION pub_merge; DROP TABLE t; RESET search_path; -- diff --git a/src/test/regress/expected/partition_split.out b/src/test/regress/expected/partition_split.out index 8e245563801..87374ca43ff 100644 --- a/src/test/regress/expected/partition_split.out +++ b/src/test/regress/expected/partition_split.out @@ -1751,6 +1751,34 @@ SELECT relname, reltablespace FROM pg_class tp_lo | 0 (2 rows) +DROP TABLE t; +-- SPLIT PARTITION carries the split partition's replica identity to the new +-- partitions. +CREATE TABLE t (i int PRIMARY KEY) PARTITION BY RANGE (i); +CREATE TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (2); +ALTER TABLE tp_0_2 REPLICA IDENTITY FULL; +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); +SELECT relname, relreplident FROM pg_class + WHERE relname IN ('tp_0_1', 'tp_1_2') ORDER BY relname; + relname | relreplident +---------+-------------- + tp_0_1 | f + tp_1_2 | f +(2 rows) + +DROP TABLE t; +-- SPLIT PARTITION rejects a partition that is directly part of a publication. +CREATE TABLE t (i int PRIMARY KEY) PARTITION BY RANGE (i); +CREATE TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (2); +CREATE PUBLICATION pub_split FOR TABLE tp_0_2; +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); -- fails +ERROR: cannot merge or split partition "tp_0_2" that is directly part of a publication +HINT: Publish the partitioned table instead, or add the new partition to the publication after the operation. +DROP PUBLICATION pub_split; DROP TABLE t; RESET search_path; -- diff --git a/src/test/regress/sql/partition_merge.sql b/src/test/regress/sql/partition_merge.sql index 80dc365b0ce..f714a1c64d5 100644 --- a/src/test/regress/sql/partition_merge.sql +++ b/src/test/regress/sql/partition_merge.sql @@ -839,6 +839,34 @@ SELECT reltablespace FROM pg_class WHERE relname = 'tp_merged'; DROP TABLE t; +-- MERGE PARTITIONS carries over a uniform replica identity ... +CREATE TABLE t (i int PRIMARY KEY) PARTITION BY RANGE (i); +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +ALTER TABLE tp_0_1 REPLICA IDENTITY FULL; +ALTER TABLE tp_1_2 REPLICA IDENTITY FULL; +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +SELECT relreplident FROM pg_class WHERE relname = 'tp_0_2'; +DROP TABLE t; + +-- ... but rejects merging partitions with different replica identities. +CREATE TABLE t (i int PRIMARY KEY) PARTITION BY RANGE (i); +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +ALTER TABLE tp_0_1 REPLICA IDENTITY FULL; +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; -- fails +DROP TABLE t; + +-- MERGE PARTITIONS rejects a partition that is directly part of a publication. +CREATE TABLE t (i int PRIMARY KEY) PARTITION BY RANGE (i); +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +CREATE PUBLICATION pub_merge FOR TABLE tp_0_1; +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; -- fails +DROP PUBLICATION pub_merge; +DROP TABLE t; + + RESET search_path; -- diff --git a/src/test/regress/sql/partition_split.sql b/src/test/regress/sql/partition_split.sql index ffd15e7f969..8734419e754 100644 --- a/src/test/regress/sql/partition_split.sql +++ b/src/test/regress/sql/partition_split.sql @@ -1256,6 +1256,28 @@ SELECT relname, reltablespace FROM pg_class WHERE relname IN ('tp_lo', 'tp_hi') ORDER BY relname; DROP TABLE t; +-- SPLIT PARTITION carries the split partition's replica identity to the new +-- partitions. +CREATE TABLE t (i int PRIMARY KEY) PARTITION BY RANGE (i); +CREATE TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (2); +ALTER TABLE tp_0_2 REPLICA IDENTITY FULL; +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); +SELECT relname, relreplident FROM pg_class + WHERE relname IN ('tp_0_1', 'tp_1_2') ORDER BY relname; +DROP TABLE t; + +-- SPLIT PARTITION rejects a partition that is directly part of a publication. +CREATE TABLE t (i int PRIMARY KEY) PARTITION BY RANGE (i); +CREATE TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (2); +CREATE PUBLICATION pub_split FOR TABLE tp_0_2; +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); -- fails +DROP PUBLICATION pub_split; +DROP TABLE t; + RESET search_path; -- -- 2.50.1 (Apple Git-155)