v3-0004-Reject-degenerate-SPLIT-PARTITION-with-DEFAULT-pa.patch
application/octet-stream
Filename: v3-0004-Reject-degenerate-SPLIT-PARTITION-with-DEFAULT-pa.patch
Type: application/octet-stream
Part: 3
From 866b82fdc77b6e5420b335588681f25acd20758c Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Mon, 18 May 2026 00:37:52 +0300
Subject: [PATCH v3 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 <lic@highgo.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Discussion: https://postgr.es/m/C18878AB-DEB2-4A61-9995-A035DD644B81@gmail.com
---
src/backend/partitioning/partbounds.c | 91 +++++++++++++++++++
src/test/regress/expected/partition_split.out | 18 ++++
src/test/regress/sql/partition_split.sql | 16 ++++
3 files changed, 125 insertions(+)
diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index 7d3580cbc10..f626dc019af 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -5700,6 +5700,87 @@ 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.
+ *
+ * Must be called after the per-partition bound validation in
+ * check_partitions_for_split() so that containment of new bounds within the
+ * split partition is already established. Given containment, RANGE bounds
+ * are equal iff their lower and upper rbounds match; LIST bound sets are
+ * equal iff their listdatums lengths match.
+ */
+static void
+check_split_partition_not_same_bound(Relation parent,
+ Oid splitPartOid,
+ SinglePartitionSpec **parts,
+ int nparts,
+ ParseState *pstate)
+{
+ PartitionKey key = RelationGetPartitionKey(parent);
+ PartitionBoundSpec *new_spec;
+ PartitionBoundSpec *split_spec;
+
+ if (nparts != 1)
+ return;
+
+ new_spec = parts[0]->bound;
+ split_spec = get_partition_bound_spec(splitPartOid);
+
+ Assert(new_spec->strategy == split_spec->strategy);
+
+ if (key->strategy == PARTITION_STRATEGY_RANGE)
+ {
+ PartitionRangeBound *new_lower;
+ PartitionRangeBound *new_upper;
+ PartitionRangeBound *split_lower;
+ PartitionRangeBound *split_upper;
+
+ new_lower = make_one_partition_rbound(key, -1, new_spec->lowerdatums, true);
+ new_upper = make_one_partition_rbound(key, -1, new_spec->upperdatums, false);
+ split_lower = make_one_partition_rbound(key, -1, split_spec->lowerdatums, true);
+ split_upper = make_one_partition_rbound(key, -1, split_spec->upperdatums, false);
+
+ if (partition_rbound_cmp(key->partnatts, key->partsupfunc,
+ key->partcollation,
+ new_lower->datums, new_lower->kind, true,
+ split_lower) != 0)
+ return;
+ if (partition_rbound_cmp(key->partnatts, key->partsupfunc,
+ key->partcollation,
+ new_upper->datums, new_upper->kind, false,
+ split_upper) != 0)
+ return;
+ }
+ else
+ {
+ Assert(key->strategy == PARTITION_STRATEGY_LIST);
+
+ /*
+ * Containment of the new partition's values within the split
+ * partition was established by the per-partition validation; thus
+ * equality of value sets reduces to equality of cardinality.
+ */
+ if (list_length(new_spec->listdatums) !=
+ list_length(split_spec->listdatums))
+ 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
*
@@ -5871,5 +5952,15 @@ check_partitions_for_split(Relation parent,
new_parts, nparts, pstate);
}
+ /*
+ * Reject the degenerate form where the single non-DEFAULT replacement
+ * partition keeps the bound of the split partition; the command then does
+ * nothing beyond adding a DEFAULT partition. Containment was established
+ * by the per-partition validation above, so an equality check is enough.
+ */
+ if (!isSplitPartDefault && createDefaultPart)
+ check_split_partition_not_same_bound(parent, splitPartOid, new_parts,
+ nparts, pstate);
+
pfree(new_parts);
}
diff --git a/src/test/regress/expected/partition_split.out b/src/test/regress/expected/partition_split.out
index 2b9a6aa50ed..7216bd9d4f9 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.39.5 (Apple Git-154)