From 4fc92276d96195c38aec4477bd14c00a514173df Mon Sep 17 00:00:00 2001 From: "Chao Li (Evan)" Date: Wed, 13 May 2026 11:17:19 +0800 Subject: [PATCH v1] Fix SPLIT PARTITION validation with DEFAULT When ALTER TABLE ... SPLIT PARTITION specified a DEFAULT partition, range bound validation checked the lower bound for the first explicit partition and the upper bound for non-first explicit partitions. As a result, when there was only one explicit non-DEFAULT partition, its upper bound was not checked. This could allow the new partition to extend beyond the split partition's bound and overlap another existing partition. Fix this by checking the upper bound whenever the explicit partition is the last one, rather than only when it is not the first one. While here, reject the degenerate form where a non-DEFAULT partition is split into one non-DEFAULT partition with exactly the same bound plus a DEFAULT partition. That form performs no real split and merely adds a DEFAULT partition through the split-partition path, for which existing commands should be used instead. Also clarify the documentation for SPLIT PARTITION with DEFAULT, and adjust the hint for explicit bounds that extend outside the split partition when a DEFAULT partition is specified. Author: Chao Li --- doc/src/sgml/ref/alter_table.sgml | 31 ++++---- src/backend/partitioning/partbounds.c | 78 +++++++++++++++++-- src/test/regress/expected/partition_split.out | 43 ++++++++++ src/test/regress/sql/partition_split.sql | 37 +++++++++ 4 files changed, 171 insertions(+), 18 deletions(-) diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index 1f9a456fd33..97a6d331776 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -1293,28 +1293,33 @@ WITH ( MODULUS numeric_literal, REM This form splits a single partition of the target table into new - partitions. Hash-partitioned target table is not supported. - Only a simple, non-partitioned partition can be split. - If the split partition is the DEFAULT partition, - one of the new partitions must be DEFAULT. - If the partitioned table does not have a DEFAULT + partitions. Hash-partitioned target table is not supported. + Only a simple, non-partitioned partition can be split. If the + split partition is the DEFAULT partition, one + of the new partitions must be DEFAULT. If the + partitioned table does not have a DEFAULT partition, a DEFAULT partition can be defined as one of the new partitions. - The bounds of new partitions should not overlap with those of new or - existing partitions (except partition_name). - The combined bounds of new partitions + The bounds of new non-DEFAULT partitions must not + overlap with those of new or existing partitions, except + partition_name, and + must not extend outside the bounds of the split partition + partition_name. + If no new DEFAULT partition is specified, the + combined bounds of the new partitions + partition_name1, partition_name2[, ...] - should be equal to the bounds of the split partition + must exactly match the bounds of the split partition partition_name. One of the new partitions can have the same name as the split partition - partition_name - (this is suitable in case of splitting the DEFAULT - partition: after the split, the DEFAULT partition - remains with the same name, but its partition bound changes). + partition_name. + This is useful when splitting the DEFAULT partition, + so that after the split, the DEFAULT partition + keeps the same name but its partition bound changes. diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c index 9b4277a4987..6ca6d8b719f 100644 --- a/src/backend/partitioning/partbounds.c +++ b/src/backend/partitioning/partbounds.c @@ -36,6 +36,7 @@ #include "utils/datum.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" +#include "utils/memutils.h" #include "utils/partcache.h" #include "utils/ruleutils.h" #include "utils/snapmgr.h" @@ -5415,11 +5416,11 @@ check_partition_bounds_for_split_range(Relation parent, errmsg("lower bound of partition \"%s\" is less than lower bound of split partition \"%s\"", relname, get_rel_name(splitPartOid)), - errhint("%s require combined bounds of new partitions must exactly match the bound of the split partition.", - "ALTER TABLE ... SPLIT PARTITION"), + errhint("Explicit partition bounds must be contained within the bounds of the split partition when a DEFAULT partition is specified."), parser_errposition(pstate, exprLocation((Node *) datum))); } - else + + if (last) { PartitionRangeBound *split_upper; @@ -5457,8 +5458,7 @@ check_partition_bounds_for_split_range(Relation parent, errmsg("upper bound of partition \"%s\" is greater than upper bound of split partition \"%s\"", relname, get_rel_name(splitPartOid)), - errhint("%s require combined bounds of new partitions must exactly match the bound of the split partition.", - "ALTER TABLE ... SPLIT PARTITION"), + errhint("Explicit partition bounds must be contained within the bounds of the split partition when a DEFAULT partition is specified."), parser_errposition(pstate, exprLocation((Node *) datum))); } } @@ -5701,6 +5701,70 @@ check_parent_values_in_new_partitions(Relation parent, } } +/* + * check_split_partition_not_same_bound + * + * Reject splitting a non-DEFAULT partition into one non-DEFAULT partition + * with the original bound plus a DEFAULT partition. That form does not + * perform a real split; it merely adds a DEFAULT partition to the parent + * table through the split-partition path. Users should use + * CREATE TABLE ... PARTITION OF ... DEFAULT or ALTER TABLE ... ATTACH + * PARTITION ... DEFAULT for that. + */ +static void +check_split_partition_not_same_bound(Relation parent, + Oid splitPartOid, + SinglePartitionSpec **parts, + int nparts, + ParseState *pstate) +{ + PartitionKey key = RelationGetPartitionKey(parent); + PartitionBoundSpec *split_spec; + PartitionBoundSpec *new_specs[1]; + PartitionBoundSpec *old_specs[1]; + PartitionBoundInfo new_boundinfo; + PartitionBoundInfo old_boundinfo; + int *new_mapping; + int *old_mapping; + MemoryContext old_cxt; + MemoryContext tmp_cxt; + bool same_bound; + + if (nparts != 1) + return; + + tmp_cxt = AllocSetContextCreate(CurrentMemoryContext, + "split partition bound comparison", + ALLOCSET_SMALL_SIZES); + old_cxt = MemoryContextSwitchTo(tmp_cxt); + + split_spec = get_partition_bound_spec(splitPartOid); + + new_specs[0] = parts[0]->bound; + new_boundinfo = partition_bounds_create(new_specs, 1, key, &new_mapping); + + old_specs[0] = split_spec; + old_boundinfo = partition_bounds_create(old_specs, 1, key, &old_mapping); + + same_bound = partition_bounds_equal(key->partnatts, key->parttyplen, + key->parttypbyval, + new_boundinfo, old_boundinfo); + + MemoryContextSwitchTo(old_cxt); + MemoryContextDelete(tmp_cxt); + + if (!same_bound) + return; + + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot split partition \"%s\" only to add a DEFAULT partition", + get_rel_name(splitPartOid)), + errdetail("The non-DEFAULT partition would keep the same partition bound."), + errhint("Use CREATE TABLE ... PARTITION OF ... DEFAULT to add a DEFAULT partition."), + parser_errposition(pstate, parts[0]->name->location)); +} + /* * check_partitions_for_split * @@ -5775,6 +5839,10 @@ check_partitions_for_split(Relation parent, Assert(nparts == list_length(partlist) - 1); } + if (!isSplitPartDefault && createDefaultPart) + check_split_partition_not_same_bound(parent, splitPartOid, new_parts, + nparts, pstate); + if (strategy == PARTITION_STRATEGY_RANGE) { PartitionRangeBound **lower_bounds; diff --git a/src/test/regress/expected/partition_split.out b/src/test/regress/expected/partition_split.out index 961b37953c8..5d7380e28bb 100644 --- a/src/test/regress/expected/partition_split.out +++ b/src/test/regress/expected/partition_split.out @@ -1188,6 +1188,49 @@ SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text DROP TABLE sales_range; -- +-- Test that SPLIT PARTITION rejects the degenerate case where the only +-- non-DEFAULT replacement partition keeps the original bound and the command +-- merely adds a DEFAULT partition. +-- +CREATE TABLE t (i int) PARTITION BY RANGE (i); +CREATE TABLE tp_0_50 PARTITION OF t FOR VALUES FROM (0) TO (50); +INSERT INTO t VALUES (1); +-- ERROR +ALTER TABLE t SPLIT PARTITION tp_0_50 INTO + (PARTITION tp_0_50 FOR VALUES FROM (0) TO (50), + PARTITION tp_default DEFAULT); +ERROR: cannot split partition "tp_0_50" only to add a DEFAULT partition +LINE 2: (PARTITION tp_0_50 FOR VALUES FROM (0) TO (50), + ^ +DETAIL: The non-DEFAULT partition would keep the same partition bound. +HINT: Use CREATE TABLE ... PARTITION OF ... DEFAULT to add a DEFAULT partition. +-- ERROR +ALTER TABLE t SPLIT PARTITION tp_0_50 INTO + (PARTITION tp_0_5 FOR VALUES FROM (0) TO (5), + PARTITION tp_6_51 FOR VALUES FROM (6) TO (51), + PARTITION tp_default DEFAULT); +ERROR: upper bound of partition "tp_6_51" is greater than upper bound of split partition "tp_0_50" +LINE 3: PARTITION tp_6_51 FOR VALUES FROM (6) TO (51), + ^ +HINT: Explicit partition bounds must be contained within the bounds of the split partition when a DEFAULT partition is specified. +DROP TABLE t; +-- +-- Test that the explicit partition bound cannot extend outside the split +-- partition's bound when a DEFAULT partition is specified. +-- +CREATE TABLE t (i int) PARTITION BY RANGE (i); +CREATE TABLE tp_0_51 PARTITION OF t FOR VALUES FROM (0) TO (51); +CREATE TABLE tp_51_100 PARTITION OF t FOR VALUES FROM (51) TO (100); +-- ERROR +ALTER TABLE t SPLIT PARTITION tp_0_51 INTO + (PARTITION tp_0_51 FOR VALUES FROM (0) TO (53), + PARTITION tp_default DEFAULT); +ERROR: upper bound of partition "tp_0_51" is greater than upper bound of split partition "tp_0_51" +LINE 2: (PARTITION tp_0_51 FOR VALUES FROM (0) TO (53), + ^ +HINT: Explicit partition bounds must be contained within the bounds of the split partition when a DEFAULT partition is specified. +DROP TABLE t; +-- -- Try to SPLIT partition of another table. -- CREATE TABLE t1(i int, t text) PARTITION BY LIST (t); diff --git a/src/test/regress/sql/partition_split.sql b/src/test/regress/sql/partition_split.sql index a110fc87867..a925dacd205 100644 --- a/src/test/regress/sql/partition_split.sql +++ b/src/test/regress/sql/partition_split.sql @@ -834,6 +834,43 @@ SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text DROP TABLE sales_range; +-- +-- Test that SPLIT PARTITION rejects the degenerate case where the only +-- non-DEFAULT replacement partition keeps the original bound and the command +-- merely adds a DEFAULT partition. +-- +CREATE TABLE t (i int) PARTITION BY RANGE (i); +CREATE TABLE tp_0_50 PARTITION OF t FOR VALUES FROM (0) TO (50); +INSERT INTO t VALUES (1); + +-- ERROR +ALTER TABLE t SPLIT PARTITION tp_0_50 INTO + (PARTITION tp_0_50 FOR VALUES FROM (0) TO (50), + PARTITION tp_default DEFAULT); + +-- ERROR +ALTER TABLE t SPLIT PARTITION tp_0_50 INTO + (PARTITION tp_0_5 FOR VALUES FROM (0) TO (5), + PARTITION tp_6_51 FOR VALUES FROM (6) TO (51), + PARTITION tp_default DEFAULT); + +DROP TABLE t; + +-- +-- Test that the explicit partition bound cannot extend outside the split +-- partition's bound when a DEFAULT partition is specified. +-- +CREATE TABLE t (i int) PARTITION BY RANGE (i); +CREATE TABLE tp_0_51 PARTITION OF t FOR VALUES FROM (0) TO (51); +CREATE TABLE tp_51_100 PARTITION OF t FOR VALUES FROM (51) TO (100); + +-- ERROR +ALTER TABLE t SPLIT PARTITION tp_0_51 INTO + (PARTITION tp_0_51 FOR VALUES FROM (0) TO (53), + PARTITION tp_default DEFAULT); + +DROP TABLE t; + -- -- Try to SPLIT partition of another table. -- -- 2.50.1 (Apple Git-155)