public inbox for [email protected]
help / color / mirror / Atom feedFrom: Chao Li <[email protected]>
To: Dmitry Koval <[email protected]>
Cc: PostgreSQL-development <[email protected]>
Cc: Alexander Korotkov <[email protected]>
Subject: Re: Fix SPLIT PARTITION bound-overlap bug and other improvements
Date: Thu, 14 May 2026 14:58:38 +0800
Message-ID: <[email protected]> (raw)
In-Reply-To: <[email protected]>
References: <[email protected]>
<[email protected]>
> On May 14, 2026, at 04:47, Dmitry Koval <[email protected]> wrote:
>
> Hi, Chao Li!
>
> Thank you for the bug report, test script, and fix!
>
> >> 0. A bound-overlap bug
>
> I think this fix should be applied without much discussion:
> ------------------------------------------------------------------------
> diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
> index 9b4277a4987..8b8f90569fe 100644
> --- a/src/backend/partitioning/partbounds.c
> +++ b/src/backend/partitioning/partbounds.c
> @@ -5419,7 +5419,8 @@ check_partition_bounds_for_split_range(Relation parent,
> "ALTER TABLE ... SPLIT PARTITION"),
> parser_errposition(pstate, exprLocation((Node *) datum)));
> }
> - else
> +
> + if (last)
> {
> PartitionRangeBound *split_upper;
> ------------------------------------------------------------------------
Thanks for your confirmation.
>
> >> 1. The documentation about splitting with a DEFAULT partition is a bit unclear
> >> ...
> >> 2. I found this hint message confusing:
> >> ...
>
> Unfortunately, I cannot comment on these points; it would be good to get the opinion of people who know English well.
I want to add one more point about these two changes.
There is a code comment saying that when a DEFAULT partition is specified, the new partition's lower bound may be greater than the original lower bound:
```
/*
* The lower bound of "spec" must equal the lower bound of the
* split partition. However, if one of the new partitions is
* DEFAULT, then it is ok for the new partition's lower bound to
* be greater than that of the split partition.
*/
```
This also indicates that the original hint message mentioning “exactly match" is wrong for the DEFAULT case.
>
>
> >> 3. SPLIT PARTITION currently provides another way to add a DEFAULT partition:
> >> ...
>
> Agreed, this is another way to add a DEFAULT partition. But I'm not sure that this way should be disabled (using the special function check_split_partition_not_same_bound)...
> Maybe it's better to keep it "as is"?
>
Yeah, this may be worth more discussion. But I think we should either reject this usage or add a fast path to avoid unnecessary creation of a new partition, data movement, etc. Otherwise, it feels more like a misuse of SPLIT PARTITION rather than a useful new alternative.
To make this patch easier to process, I split it into 4 commits:
0001 - Fixes the bound-overlap bug
0002 - Fix the incorrect HINT message for the DEFAULT case
0003 - Fix the incorrect description about combined bound in the SGML doc
0004 - Reject only-create-default-partition usage
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
Attachments:
[application/octet-stream] v2-0001-Fix-SPLIT-PARTITION-range-bound-validation-with-D.patch (4.0K, 2-v2-0001-Fix-SPLIT-PARTITION-range-bound-validation-with-D.patch)
download | inline diff:
From 0f296d8bef250e38eee74d58cbfc04e6c9c5ea7b Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <[email protected]>
Date: Thu, 14 May 2026 13:49:43 +0800
Subject: [PATCH v2 1/4] Fix SPLIT PARTITION range bound validation with
DEFAULT
When splitting a range partition and defining a new DEFAULT partition, the
validation checked the lower bound of the first explicit partition and the
upper bound of explicit partitions only when they were not first. If there
was exactly one explicit non-DEFAULT partition, its upper bound was therefore
not checked.
This could allow the replacement partition to extend beyond the upper bound
of the partition being split, potentially overlapping another existing
partition.
Fix this by checking the upper bound whenever the explicit partition is the
last one. Add a regression test covering the single explicit partition plus
DEFAULT case.
Author: Chao Li <[email protected]>
Reviewed-by: Kirill Reshke <[email protected]>
Reviewed-by: Zhenwei Shang <[email protected]>
Reviewed-by: Dmitry Koval <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
src/backend/partitioning/partbounds.c | 3 ++-
src/test/regress/expected/partition_split.out | 16 ++++++++++++++++
src/test/regress/sql/partition_split.sql | 15 +++++++++++++++
3 files changed, 33 insertions(+), 1 deletion(-)
diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index a09beec34d8..73dea0375be 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -5419,7 +5419,8 @@ check_partition_bounds_for_split_range(Relation parent,
"ALTER TABLE ... SPLIT PARTITION"),
parser_errposition(pstate, exprLocation((Node *) datum)));
}
- else
+
+ if (last)
{
PartitionRangeBound *split_upper;
diff --git a/src/test/regress/expected/partition_split.out b/src/test/regress/expected/partition_split.out
index 961b37953c8..a2ccbe5138b 100644
--- a/src/test/regress/expected/partition_split.out
+++ b/src/test/regress/expected/partition_split.out
@@ -1188,6 +1188,22 @@ SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text
DROP TABLE sales_range;
--
+-- 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: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition.
+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..d9821c5e2a3 100644
--- a/src/test/regress/sql/partition_split.sql
+++ b/src/test/regress/sql/partition_split.sql
@@ -834,6 +834,21 @@ SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text
DROP TABLE sales_range;
+--
+-- 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)
[application/octet-stream] v2-0002-Fix-SPLIT-PARTITION-hint-for-DEFAULT-partition-bo.patch (3.4K, 3-v2-0002-Fix-SPLIT-PARTITION-hint-for-DEFAULT-partition-bo.patch)
download | inline diff:
From 1a30c86cdc81482793298602f89d03821207339c Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <[email protected]>
Date: Thu, 14 May 2026 13:53:25 +0800
Subject: [PATCH v2 2/4] Fix SPLIT PARTITION hint for DEFAULT partition bounds
When ALTER TABLE ... SPLIT PARTITION specifies a DEFAULT partition, the
explicit partitions do not need to cover the split partition's bound
exactly. They may cover only part of it, with the DEFAULT partition
covering the remaining range.
However, the existing hint said that the combined bounds of the new
partitions must exactly match the bound of the split partition, which is
misleading for this case and inconsistent with the code comment.
Fix the hint to state the actual requirement: explicit partition bounds
must stay within the bounds of the split partition when a DEFAULT
partition is specified.
Author: Chao Li <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/partitioning/partbounds.c | 6 ++----
src/test/regress/expected/partition_split.out | 2 +-
2 files changed, 3 insertions(+), 5 deletions(-)
diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index 73dea0375be..19589dc687f 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -5415,8 +5415,7 @@ 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)));
}
@@ -5458,8 +5457,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)));
}
}
diff --git a/src/test/regress/expected/partition_split.out b/src/test/regress/expected/partition_split.out
index a2ccbe5138b..d4f536c5e00 100644
--- a/src/test/regress/expected/partition_split.out
+++ b/src/test/regress/expected/partition_split.out
@@ -1201,7 +1201,7 @@ ALTER TABLE t SPLIT PARTITION tp_0_51 INTO
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: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition.
+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.
--
2.50.1 (Apple Git-155)
[application/octet-stream] v2-0003-Clarify-SPLIT-PARTITION-bound-requirements-in-doc.patch (3.8K, 4-v2-0003-Clarify-SPLIT-PARTITION-bound-requirements-in-doc.patch)
download | inline diff:
From d2e41d07334bca65820d7840c68bfafac2eac6fd Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <[email protected]>
Date: Thu, 14 May 2026 13:51:19 +0800
Subject: [PATCH v2 3/4] Clarify SPLIT PARTITION bound requirements in docs
The documentation said that the bounds of new partitions should not
overlap and that their combined bounds should equal the bounds of the
split partition. That is misleading when a new DEFAULT partition is
specified, because the explicit partitions may cover only part of the
split partition while the DEFAULT partition covers the rest.
Clarify that new non-DEFAULT partition bounds must not overlap with
other new or existing partitions and must be contained within the bounds
of the split partition. Also state that the combined bounds must exactly
match the split partition only when no new DEFAULT partition is specified.
While here, improve nearby wording about hash-partitioned target tables
and splitting a DEFAULT partition with the same partition name.
Author: Chao Li <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
doc/src/sgml/ref/alter_table.sgml | 23 ++++++++++++++---------
1 file changed, 14 insertions(+), 9 deletions(-)
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 1f9a456fd33..dec34337d1a 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -1293,7 +1293,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<listitem>
<para>
This form splits a single partition of the target table into new
- partitions. Hash-partitioned target table is not supported.
+ partitions. Hash-partitioned target tables are not supported.
Only a simple, non-partitioned partition can be split.
If the split partition is the <literal>DEFAULT</literal> partition,
one of the new partitions must be <literal>DEFAULT</literal>.
@@ -1303,18 +1303,23 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
- The bounds of new partitions should not overlap with those of new or
- existing partitions (except <replaceable class="parameter">partition_name</replaceable>).
- The combined bounds of new partitions <literal>
+ The bounds of new non-<literal>DEFAULT</literal> partitions must not
+ overlap with those of new or existing partitions, except
+ <replaceable class="parameter">partition_name</replaceable>, and must be
+ contained within the bounds of the split partition
+ <replaceable class="parameter">partition_name</replaceable>.
+ If no new <literal>DEFAULT</literal> partition is specified, the
+ combined bounds of the new partitions
+ <literal>
<replaceable class="parameter">partition_name1</replaceable>,
<replaceable class="parameter">partition_name2</replaceable>[, ...]
- </literal> should be equal to the bounds of the split partition
+ </literal> must exactly match the bounds of the split partition
<replaceable class="parameter">partition_name</replaceable>.
One of the new partitions can have the same name as the split partition
- <replaceable class="parameter">partition_name</replaceable>
- (this is suitable in case of splitting the <literal>DEFAULT</literal>
- partition: after the split, the <literal>DEFAULT</literal> partition
- remains with the same name, but its partition bound changes).
+ <replaceable class="parameter">partition_name</replaceable>.
+ This is useful when splitting the <literal>DEFAULT</literal> partition,
+ so that after the split, the <literal>DEFAULT</literal> partition
+ keeps the same name but its partition bound changes.
</para>
<para>
--
2.50.1 (Apple Git-155)
[application/octet-stream] v2-0004-Reject-degenerate-SPLIT-PARTITION-with-DEFAULT-pa.patch (6.5K, 5-v2-0004-Reject-degenerate-SPLIT-PARTITION-with-DEFAULT-pa.patch)
download | inline diff:
From 95cb0af42d75d23050b2f5ce01325863b7583e5c Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <[email protected]>
Date: Thu, 14 May 2026 13:55:37 +0800
Subject: [PATCH v2 4/4] Reject degenerate SPLIT PARTITION with DEFAULT
partition
ALTER TABLE ... SPLIT PARTITION allows a DEFAULT partition to be created
as one of the replacement partitions when the parent table does not
already have one. However, it should not allow the degenerate case where
a non-DEFAULT partition keeps exactly the same bound as the split
partition and the command merely adds a DEFAULT partition through the
SPLIT PARTITION path.
Detect that case by comparing the bound of the split partition with the
bound of the only non-DEFAULT replacement partition, and raise an error
when they are the same. Users should add a DEFAULT partition directly
with CREATE TABLE ... PARTITION OF ... DEFAULT or ALTER TABLE ... ATTACH
PARTITION ... DEFAULT instead.
Author: Chao Li <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/partitioning/partbounds.c | 69 +++++++++++++++++++
src/test/regress/expected/partition_split.out | 18 +++++
src/test/regress/sql/partition_split.sql | 16 +++++
3 files changed, 103 insertions(+)
diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index 19589dc687f..5c1919e6d10 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"
@@ -5700,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
*
@@ -5774,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 d4f536c5e00..6869a65badb 100644
--- a/src/test/regress/expected/partition_split.out
+++ b/src/test/regress/expected/partition_split.out
@@ -1188,6 +1188,24 @@ 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.
+DROP TABLE t;
+--
-- Test that the explicit partition bound cannot extend outside the split
-- partition's bound when a DEFAULT partition is specified.
--
diff --git a/src/test/regress/sql/partition_split.sql b/src/test/regress/sql/partition_split.sql
index d9821c5e2a3..e7bbcc9f054 100644
--- a/src/test/regress/sql/partition_split.sql
+++ b/src/test/regress/sql/partition_split.sql
@@ -834,6 +834,22 @@ 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);
+
+DROP TABLE t;
+
--
-- Test that the explicit partition bound cannot extend outside the split
-- partition's bound when a DEFAULT partition is specified.
--
2.50.1 (Apple Git-155)
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: [email protected]
Cc: [email protected], [email protected], [email protected]
Subject: Re: Fix SPLIT PARTITION bound-overlap bug and other improvements
In-Reply-To: <[email protected]>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox