v2-0004-Reject-degenerate-SPLIT-PARTITION-with-DEFAULT-pa.patch

application/octet-stream

Filename: v2-0004-Reject-degenerate-SPLIT-PARTITION-with-DEFAULT-pa.patch
Type: application/octet-stream
Part: 3
Message: Re: Fix SPLIT PARTITION bound-overlap bug and other improvements

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: format-patch
Series: patch v2-0004
Subject: Reject degenerate SPLIT PARTITION with DEFAULT partition
File+
src/backend/partitioning/partbounds.c 69 0
src/test/regress/expected/partition_split.out 18 0
src/test/regress/sql/partition_split.sql 16 0
From 95cb0af42d75d23050b2f5ce01325863b7583e5c Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <lic@highgo.com>
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 <lic@highgo.com>
Reviewed-by:
Discussion: https://postgr.es/m/C18878AB-DEB2-4A61-9995-A035DD644B81@gmail.com
---
 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)