Subject: Re: Add SPLIT PARTITION/MERGE PARTITIONS commands

From: Robert Haas <[email-masked]>
Date: Thu, 22 Aug 2024 13:25:07 -0400

On Thu, Aug 22, 2024 at 12:43 PM Alexander Korotkov
<[email-masked]> wrote:
> Thank you for your feedback.  Yes, it seems that there is not enough
> time to even carefully analyze all the issues in these features.  The
> rule of thumb I can get from this experience is "think multiple times
> before accessing something already opened by its name".  I'm going to
> revert these features during next couple days.

Thanks, and sorry about that. I would say even "think multiple times"
is possibly not strong enough -- it might almost be "just don't ever
do it". Even if (in some particular case) the invalidation mechanism
seems to protect you from getting wrong answers, there are often holes
in that, specifically around search_path = foo, bar and you're
operating on an object in schema bar and an identically-named object
is created in schema foo at just the wrong time. Sometimes there are
problems even when search_path is not involved, but when it is, there
are more.

Here, aside from the name lookup issues, there are also problems with
expression evaluation: we can't split partitions without reindexing
rows that those partitions contain, and it is critical to think
through which is going to do the evaluation and make sure it's
properly sandboxed. I think we might need
SECURITY_RESTRICTED_OPERATION here.

Another thing I want to highlight if you do have another go at this
patch is that it's really critical to think about where every single
property of the newly-created tables comes from. The original patch
didn't consider relpersistence or tableam, and here I just discovered
that owner is also an issue that probably needs more consideration,
but it goes way beyond that. For example, I was surprised to discover
that if I put per-partition constraints or triggers on a partition and
then split it, they were not duplicated to the new partitions. Now,
maybe that's actually the behavior we want -- I'm not 100% positive --
but it sure wasn't what I was expecting. If we did duplicate them when
splitting, then what's supposed to happen when merging occurs? That is
not at all obvious, at least to me, but it needs careful thought. ACLs
and rules and default values and foreign keys (both outbond and
inbound) all need to be considered too, along with 27 other things
that I'm sure I'm not thinking about right now. Some of this behavior
should probably be explicitly documented, but all of it should be
considered carefully enough before commit to avoid surprises later. I
say that both from a security point of view and also just from a user
experience point of view. Even if things aren't insecure, they can
still be annoying, but it's not uncommon in cases like this for
annoying things to turn out to also be insecure.

Finally, if you do revisit this, I believe it would be a good idea to
think a bit harder about how data is moved around. My impression (and
please correct me if I am mistaken) is that currently, any split or
merge operation rewrites all the data in the source partition(s). If a
large partition is being split nearly equally, I think that has a good
chance of being optimal, but I think that might be the only case. If
we're merging partitions, wouldn't it be better to adjust the
constraints on the first partition -- or perhaps the largest partition
if we want to be clever -- and insert the data from all of the others
into it? Maybe that would even have syntax that puts the user in
control of which partition survives, e.g. ALTER TABLE tab1 MERGE
PARTITION part1 WITH part2, part3, .... That would also make it really
obvious to the user what all of the properties of part1 will be after
the merge: they will be exactly the same as they were before the
merge, except that the partition constraint will have been adjusted.
You basically dodge everything in the previous paragraph in one shot,
and it seems like it would also be faster. Splitting there's no
similar get-out-of-jail free card, at least not that I can see. Even
if you add syntax that splits a partition by using INSERT/DELETE to
move some rows to a newly-created partition, you still have to make at
least one new partition. But possibly that syntax is worth having
anyway, because it would be a lot quicker in the case of a highly
asymmetric split. On the other hand, maybe even splits are much more
likely and we don't really need it. I don't know.

-- 
Robert Haas
EDB: http://www.enterprisedb.com

---

From: Alexander Korotkov <[email-masked]>
Date: Fri, 23 Aug 2024 03:56:23 +0300

> likely and we don't really need it. I don't know.

Thank you for so valuable feedback!  When I have another go over this
patch I will ensure this is addressed.

------
Regards,
Alexander Korotkov
Supabase

---

From: Dmitry Koval <[email-masked]>
Date: Tue, 27 Aug 2024 21:24:35 +0300

Hi!

Alexander Korotkov, Robert Haas - thanks for fixes and feedbacks!

This email is a starting point for further work.
There are two files attached to this email:
v32-0001-Implement-ALTER-TABLE-.-MERGE-PARTITIONS-.-comma.patch,
v32-0002-Implement-ALTER-TABLE-.-SPLIT-PARTITION-.-comman.patch.

They contains changes from reverted commits 1adf16b8fb, 87c21bb941, and
subsequent fixes and improvements including df64c81ca9, c99ef1811a,
9dfcac8e15, 885742b9f8, 842c9b2705, fcf80c5d5f, 96c7381c4c, f4fc7cb54b,
60ae37a8bc, 259c96fa8f, 449cdcd486, 3ca43dbbb6, 2a679ae94e, 3a82c689fd,
fbd4321fd5, d53a4286d7, c086896625, 4e5d6c4091.
I didn't include fix 04158e7fa3 into patches because Robert Haas 
objected to its use.

A short list of known issues and questions (see more details in [1] and 
[2]):

1. Function createPartitionTable() should be rewritten using partitioned 
table OID (not name) and without using ProcessUtility().

2. Should it be considered an error when we split a partition owned by 
another user and get partitions that owned by our user?
(I think this is not a problem. Perhaps disallow merging other users' 
partitions would be too strict a restriction.)

3. About the functional index "create index on foo (run_me(a));".
(Should we disallow merging of another user's partitions when 
partitioned table has functional indexes? SECURITY_RESTRICTED_OPERATION?)

4. Need to decide what is correct in case there are per-partition 
constraints or triggers on a split partition. They not duplicated to the 
new partitions now. (But might be in this case we should have an error 
or warning?)

5. "If we're merging partitions, wouldn't it be better to adjust the 
constraints on the first partition - or perhaps the largest partition if 
we want to be clever -- and insert the data from all of the others into 
it? Maybe that would even have syntax that puts the user in control of 
which partition survives, e.g. ALTER TABLE tab1 MERGE PARTITION part1 
WITH part2, part3, .... That would also make it really obvious to the 
user what all of the properties of part1 will be after the merge: they 
will be exactly the same as they were before the merge, except that the 
partition constraint will have been adjusted."
(Similar optimization was proposed in [3] but was rejected [4]).

Links.
[1] 
https://www.postgresql.org/message-id/CA%2BTgmobHYix%3DNn8D4RUHa6fhUVPR88KGAMq1pBfnGfOfEjRixA%40mail.gmail.com.
[2] 
https://www.postgresql.org/message-id/CA%2BTgmoY0%3DbT_xBP8csR%3DMFE%3DFxGE2n2-me2-31jBOgEcLvW7ug%40mail.gmail.com
[3] 
https://www.postgresql.org/message-id/c3730d78-6081-4c41-9715-d1d192734576%40postgrespro.ru, 
see v31-0003-Additional-patch-for-ALTER-TABLE-.-MERGE-PARTITI.patch
[4] 
https://www.postgresql.org/message-id/CAPpHfdtj7YsPaASoVPN%2BN3H4_Ct%2BkQw8QY1d_9u7FPnbghkicw%40mail.gmail.com

-- 
With best regards,
Dmitry Koval

Postgres Professional: http://postgrespro.com

From a934c8855cb1b05474a34a4417eb0ed1c00bc5db Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:57:22 +0300
Subject: [PATCH v32 1/2] Implement ALTER TABLE ... MERGE PARTITIONS ...
 command

This new DDL command merges several partitions into the one partition of the
target table.  The target partition is created using new
createPartitionTable() function with parent partition as the template.

This commit comprises quite naive implementation which works in single process
and holds the ACCESS EXCLUSIVE LOCK on the parent table during all the
operations including the tuple routing.  This is why this new DDL command
can't be recommended for large partitioned tables under a high load.  However,
this implementation come in handy in certain cases even as is.
Also, it could be used as a foundation for future implementations with lesser
locking and possibly parallel.

Discussion: https://postgr.es/m/c73a1746-0cd0-6bdd-6b23-3ae0b7c0c582%40postgrespro.ru
Author: Dmitry Koval
Reviewed-by: Matthias van de Meent, Laurenz Albe, Zhihong Yu, Justin Pryzby
Reviewed-by: Alvaro Herrera, Robert Haas, Stephane Tachoires

Fixes (summary information).
Authors: Alexander Korotkov, Tender Wang, Richard Guo, Dagfinn Ilmari Mannsaker
Authors: Fujii Masao
Reviewed-by: Alexander Korotkov, Robert Haas, Justin Pryzby, Pavel Borisov
Reviewed-by: Masahiko Sawada
Reported-by: Alexander Lakhin, Justin Pryzby, Kyotaro Horiguchi
Reported-by: Daniel Gustafsson, Tom Lane, Noah Misch
---
 doc/src/sgml/ddl.sgml                         |  19 +
 doc/src/sgml/ref/alter_table.sgml             |  90 +-
 src/backend/commands/tablecmds.c              | 420 +++++++-
 src/backend/parser/gram.y                     |  22 +-
 src/backend/parser/parse_utilcmd.c            | 131 +++
 src/backend/partitioning/partbounds.c         | 212 +++-
 src/bin/psql/tab-complete.c                   |  10 +
 src/include/nodes/parsenodes.h                |  14 +
 src/include/parser/kwlist.h                   |   1 +
 src/include/partitioning/partbounds.h         |   6 +
 .../isolation/expected/partition-merge.out    | 199 ++++
 src/test/isolation/isolation_schedule         |   1 +
 src/test/isolation/specs/partition-merge.spec |  54 +
 .../test_ddl_deparse/test_ddl_deparse.c       |   3 +
 src/test/regress/expected/partition_merge.out | 945 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/partition_merge.sql      | 609 +++++++++++
 src/tools/pgindent/typedefs.list              |   1 +
 18 files changed, 2715 insertions(+), 24 deletions(-)
 create mode 100644 src/test/isolation/expected/partition-merge.out
 create mode 100644 src/test/isolation/specs/partition-merge.spec
 create mode 100644 src/test/regress/expected/partition_merge.out
 create mode 100644 src/test/regress/sql/partition_merge.sql

diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index b671858627..2770e81656 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -4353,6 +4353,25 @@ ALTER TABLE measurement_y2006m02 ADD UNIQUE (city_id, logdate);
 ALTER INDEX measurement_city_id_logdate_key
     ATTACH PARTITION measurement_y2006m02_city_id_logdate_key;
 ...
+</programlisting>
+    </para>
+
+    <para>
+     There is also an option for merging multiple table partitions into
+     a single partition using the
+     <link linkend="sql-altertable-merge-partitions"><command>ALTER TABLE ... MERGE PARTITIONS</command></link>.
+     This feature simplifies the management of partitioned tables by allowing
+     users to combine partitions that are no longer needed as
+     separate entities.  It's important to note that this operation is not
+     supported for hash-partitioned tables and acquires an
+     <literal>ACCESS EXCLUSIVE</literal> lock, which could impact high-load
+     systems due to the lock's restrictive nature.  For example, we can
+     merge three monthly partitions into one quarter partition:
+<programlisting>
+ALTER TABLE measurement
+    MERGE PARTITIONS (measurement_y2006m01,
+                      measurement_y2006m02,
+                      measurement_y2006m03) INTO measurement_y2006q1;
 </programlisting>
     </para>
    </sect3>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 1a49f321cf..b99d4972ac 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -37,6 +37,9 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
     ATTACH PARTITION <replaceable class="parameter">partition_name</replaceable> { FOR VALUES <replaceable class="parameter">partition_bound_spec</replaceable> | DEFAULT }
 ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
     DETACH PARTITION <replaceable class="parameter">partition_name</replaceable> [ CONCURRENTLY | FINALIZE ]
+ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
+    MERGE PARTITIONS (<replaceable class="parameter">partition_name1</replaceable>, <replaceable class="parameter">partition_name2</replaceable> [, ...])
+        INTO <replaceable class="parameter">partition_name</replaceable>
 
 <phrase>where <replaceable class="parameter">action</replaceable> is one of:</phrase>
 
@@ -1117,14 +1120,87 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-altertable-merge-partitions">
+    <term><literal>MERGE PARTITIONS (<replaceable class="parameter">partition_name1</replaceable>, <replaceable class="parameter">partition_name2</replaceable> [, ...]) INTO <replaceable class="parameter">partition_name</replaceable></literal></term>
+
+    <listitem>
+     <para>
+      This form merges several partitions into the one partition of the target table.
+      Hash-partitioning is not supported.  If DEFAULT partition is not in the
+      list of partitions <replaceable class="parameter">partition_name1</replaceable>,
+      <replaceable class="parameter">partition_name2</replaceable> [, ...]:
+      <itemizedlist>
+       <listitem>
+        <para>
+         For range-partitioned tables it is necessary that the ranges
+         of the partitions <replaceable class="parameter">partition_name1</replaceable>,
+         <replaceable class="parameter">partition_name2</replaceable> [, ...] can
+         be merged into one range without spaces and overlaps (otherwise an error
+         will be generated).  The combined range will be the range for the partition
+         <replaceable class="parameter">partition_name</replaceable>.
+        </para>
+       </listitem>
+       <listitem>
+        <para>
+         For list-partitioned tables the value lists of all partitions
+         <replaceable class="parameter">partition_name1</replaceable>,
+         <replaceable class="parameter">partition_name2</replaceable> [, ...] are
+         combined and form the list of values of partition
+         <replaceable class="parameter">partition_name</replaceable>.
+        </para>
+       </listitem>
+      </itemizedlist>
+      If DEFAULT partition is in the list of partitions <replaceable class="parameter">partition_name1</replaceable>,
+      <replaceable class="parameter">partition_name2</replaceable> [, ...]:
+      <itemizedlist>
+       <listitem>
+        <para>
+         The partition <replaceable class="parameter">partition_name</replaceable>
+         will be the DEFAULT partition.
+        </para>
+       </listitem>
+       <listitem>
+        <para>
+         For range- and list-partitioned tables the ranges and lists of values
+         of the merged partitions can be any.
+        </para>
+       </listitem>
+      </itemizedlist>
+      The new partition <replaceable class="parameter">partition_name</replaceable>
+      can have the same name as one of the merged partitions.  Only simple,
+      non-partitioned partitions can be merged.
+     </para>
+     <para>
+      The new partition will be created the same as a table created with the
+      SQL command <literal>CREATE TABLE <replaceable class="parameter">partition_name</replaceable> (LIKE <replaceable class="parameter">name</replaceable> INCLUDING ALL EXCLUDING INDEXES EXCLUDING IDENTITY EXCLUDING STATISTICS)</literal>.
+      The indexes and identity are created later, after moving the data
+      into the new partition.
+      Extended statistics aren't copied from the parent table, for consistency with
+      <command>CREATE TABLE PARTITION OF</command>.
+      The new partition will have the same table access method as the parent.
+      If the parent table is persistent then the new partition is created
+      persistent.  If the parent table is temporary then the new partition
+      is also created temporary.  The new partition will also be created in
+      the same tablespace as the parent.
+     </para>
+     <note>
+      <para>
+       This command acquires an <literal>ACCESS EXCLUSIVE</literal> lock.
+       This is a significant limitation, which limits the usage of this
+       command with large partitioned tables under a high load.
+      </para>
+     </note>
+    </listitem>
+   </varlistentry>
+
   </variablelist>
   </para>
 
   <para>
    All the forms of ALTER TABLE that act on a single table, except
    <literal>RENAME</literal>, <literal>SET SCHEMA</literal>,
-   <literal>ATTACH PARTITION</literal>, and
-   <literal>DETACH PARTITION</literal> can be combined into
+   <literal>ATTACH PARTITION</literal>, <literal>DETACH PARTITION</literal>,
+   and <literal>MERGE PARTITIONS</literal> can be combined into
    a list of multiple alterations to be applied together.  For example, it
    is possible to add several columns and/or alter the type of several
    columns in a single command.  This is particularly useful with large
@@ -1367,7 +1443,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       <term><replaceable class="parameter">partition_name</replaceable></term>
       <listitem>
        <para>
-        The name of the table to attach as a new partition or to detach from this table.
+        The name of the table to attach as a new partition or to detach from this table,
+        or the name of the new merged partition.
        </para>
       </listitem>
      </varlistentry>
@@ -1783,6 +1860,13 @@ ALTER TABLE measurement
     DETACH PARTITION measurement_y2015m12;
 </programlisting></para>
 
+  <para>
+   To merge several partitions into one partition of the target table:
+<programlisting>
+ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_central)
+    INTO sales_all;
+</programlisting></para>
+
  </refsect1>
 
  <refsect1>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index dac39df83a..4d24278495 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -657,6 +657,8 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, const char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);
 
+static void ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
+								  PartitionCmd *cmd, AlterTableUtilityContext *context);
 
 /* ----------------------------------------------------------------
  *		DefineRelation
@@ -4676,6 +4678,10 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = AccessShareLock;
 				break;
 
+			case AT_MergePartitions:
+				cmd_lockmode = AccessExclusiveLock;
+				break;
+
 			default:			/* oops */
 				elog(ERROR, "unrecognized alter table type: %d",
 					 (int) cmd->subtype);
@@ -5093,6 +5099,11 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* No command-specific prep needed */
 			pass = AT_PASS_MISC;
 			break;
+		case AT_MergePartitions:
+			ATSimplePermissions(cmd->subtype, rel, ATT_TABLE);
+			/* No command-specific prep needed */
+			pass = AT_PASS_MISC;
+			break;
 		default:				/* oops */
 			elog(ERROR, "unrecognized alter table type: %d",
 				 (int) cmd->subtype);
@@ -5489,6 +5500,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 		case AT_DetachPartitionFinalize:
 			address = ATExecDetachPartitionFinalize(rel, ((PartitionCmd *) cmd->def)->name);
 			break;
+		case AT_MergePartitions:
+			cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode,
+									  cur_pass, context);
+			Assert(cmd != NULL);
+			Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
+			ATExecMergePartitions(wqueue, tab, rel, (PartitionCmd *) cmd->def,
+								  context);
+			break;
 		default:				/* oops */
 			elog(ERROR, "unrecognized alter table type: %d",
 				 (int) cmd->subtype);
@@ -6477,6 +6496,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
 			return "DETACH PARTITION";
 		case AT_DetachPartitionFinalize:
 			return "DETACH PARTITION ... FINALIZE";
+		case AT_MergePartitions:
+			return "MERGE PARTITIONS";
 		case AT_AddIdentity:
 			return "ALTER COLUMN ... ADD IDENTITY";
 		case AT_SetIdentity:
@@ -18285,6 +18306,37 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
 	}
 }
 
+/*
+ * attachPartitionTable: attach a new partition to the partitioned table
+ *
+ * wqueue: the ALTER TABLE work queue; can be NULL when not running as part
+ *   of an ALTER TABLE sequence.
+ * rel: partitioned relation;
+ * attachrel: relation of attached partition;
+ * bound: bounds of attached relation.
+ */
+static void
+attachPartitionTable(List **wqueue, Relation rel, Relation attachrel, PartitionBoundSpec *bound)
+{
+	/* OK to create inheritance.  Rest of the checks performed there */
+	CreateInheritance(attachrel, rel, true);
+
+	/* Update the pg_class entry. */
+	StorePartitionBound(attachrel, rel, bound);
+
+	/* Ensure there exists a correct set of indexes in the partition. */
+	AttachPartitionEnsureIndexes(wqueue, rel, attachrel);
+
+	/* and triggers */
+	CloneRowTriggersToPartition(rel, attachrel);
+
+	/*
+	 * Clone foreign key constraints.  Callee is responsible for setting up
+	 * for phase 3 constraint verification.
+	 */
+	CloneForeignKeyConstraints(wqueue, rel, attachrel);
+}
+
 /*
  * ALTER TABLE <name> ATTACH PARTITION <partition-name> FOR VALUES
  *
@@ -18487,23 +18539,8 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
 	check_new_partition_bound(RelationGetRelationName(attachrel), rel,
 							  cmd->bound, pstate);
 
-	/* OK to create inheritance.  Rest of the checks performed there */
-	CreateInheritance(attachrel, rel, true);
-
-	/* Update the pg_class entry. */
-	StorePartitionBound(attachrel, rel, cmd->bound);
-
-	/* Ensure there exists a correct set of indexes in the partition. */
-	AttachPartitionEnsureIndexes(wqueue, rel, attachrel);
-
-	/* and triggers */
-	CloneRowTriggersToPartition(rel, attachrel);
-
-	/*
-	 * Clone foreign key constraints.  Callee is responsible for setting up
-	 * for phase 3 constraint verification.
-	 */
-	CloneForeignKeyConstraints(wqueue, rel, attachrel);
+	/* Attach a new partition to the partitioned table. */
+	attachPartitionTable(wqueue, rel, attachrel, cmd->bound);
 
 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -20018,3 +20055,352 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+ * createPartitionTable: create table for a new partition with given name
+ * (newPartName) like table (modelRel)
+ *
+ * Emulates command: CREATE [TEMP] TABLE <newPartName> (LIKE <modelRel's name>
+ * INCLUDING ALL EXCLUDING INDEXES EXCLUDING IDENTITY EXCLUDING STATISTICS)
+ *
+ * Also, this function sets the new partition access method same as parent
+ * table access methods (similarly to CREATE TABLE ... PARTITION OF).  It
+ * checks that parent and child tables have compatible persistence.
+ *
+ * Function returns the created relation (locked in AccessExclusiveLock mode).
+ */
+static Relation
+createPartitionTable(RangeVar *newPartName, Relation modelRel,
+					 AlterTableUtilityContext *context)
+{
+	CreateStmt *createStmt;
+	TableLikeClause *tlc;
+	PlannedStmt *wrapper;
+	Relation	newRel;
+
+	/* If existing rel is temp, it must belong to this session */
+	if (modelRel->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
+		!modelRel->rd_islocaltemp)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("cannot create as partition of temporary relation of another session")));
+
+	/* New partition should have the same persistence as modelRel */
+	newPartName->relpersistence = modelRel->rd_rel->relpersistence;
+
+	createStmt = makeNode(CreateStmt);
+	createStmt->relation = newPartName;
+	createStmt->tableElts = NIL;
+	createStmt->inhRelations = NIL;
+	createStmt->constraints = NIL;
+	createStmt->options = NIL;
+	createStmt->oncommit = ONCOMMIT_NOOP;
+	createStmt->tablespacename = get_tablespace_name(modelRel->rd_rel->reltablespace);
+	createStmt->if_not_exists = false;
+	createStmt->accessMethod = get_am_name(modelRel->rd_rel->relam);
+
+	tlc = makeNode(TableLikeClause);
+	tlc->relation = makeRangeVar(get_namespace_name(RelationGetNamespace(modelRel)),
+								 RelationGetRelationName(modelRel), -1);
+
+	/*
+	 * Indexes will be inherited on "attach new partitions" stage, after data
+	 * moving.  We also don't copy the extended statistics for consistency
+	 * with CREATE TABLE PARTITION OF.
+	 */
+	tlc->options = CREATE_TABLE_LIKE_ALL &
+		~(CREATE_TABLE_LIKE_INDEXES | CREATE_TABLE_LIKE_IDENTITY | CREATE_TABLE_LIKE_STATISTICS);
+	tlc->relationOid = InvalidOid;
+	createStmt->tableElts = lappend(createStmt->tableElts, tlc);
+
+	/* Need to make a wrapper PlannedStmt. */
+	wrapper = makeNode(PlannedStmt);
+	wrapper->commandType = CMD_UTILITY;
+	wrapper->canSetTag = false;
+	wrapper->utilityStmt = (Node *) createStmt;
+	wrapper->stmt_location = context->pstmt->stmt_location;
+	wrapper->stmt_len = context->pstmt->stmt_len;
+
+	ProcessUtility(wrapper,
+				   context->queryString,
+				   false,
+				   PROCESS_UTILITY_SUBCOMMAND,
+				   NULL,
+				   NULL,
+				   None_Receiver,
+				   NULL);
+
+	/*
+	 * Open the new partition with no lock, because we already have
+	 * AccessExclusiveLock placed there after creation.
+	 */
+	newRel = table_openrv(newPartName, NoLock);
+
+	/*
+	 * We intended to create the partition with the same persistence as the
+	 * parent table, but we still need to recheck because that might be
+	 * affected by the search_path.  If the parent is permanent, so must be
+	 * all of its partitions.
+	 */
+	if (modelRel->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
+		newRel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("cannot create a temporary relation as partition of permanent relation \"%s\"",
+						RelationGetRelationName(modelRel))));
+
+	/* Permanent rels cannot be partitions belonging to temporary parent */
+	if (newRel->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
+		modelRel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("cannot create a permanent relation as partition of temporary relation \"%s\"",
+						RelationGetRelationName(modelRel))));
+
+	return newRel;
+}
+
+/*
+ * moveMergedTablesRows: scan partitions to be merged (mergingPartitionsList)
+ * of the partitioned table (rel) and move rows into the new partition
+ * (newPartRel).
+ */
+static void
+moveMergedTablesRows(Relation rel, List *mergingPartitionsList,
+					 Relation newPartRel)
+{
+	CommandId	mycid;
+
+	/* The FSM is empty, so don't bother using it. */
+	int			ti_options = TABLE_INSERT_SKIP_FSM;
+	ListCell   *listptr;
+	BulkInsertState bistate;	/* state of bulk inserts for partition */
+	TupleTableSlot *dstslot;
+
+	mycid = GetCurrentCommandId(true);
+
+	/* Prepare a BulkInsertState for table_tuple_insert. */
+	bistate = GetBulkInsertState();
+
+	/* Create necessary tuple slot. */
+	dstslot = MakeSingleTupleTableSlot(RelationGetDescr(newPartRel),
+									   table_slot_callbacks(newPartRel));
+	ExecStoreAllNullTuple(dstslot);
+
+	foreach(listptr, mergingPartitionsList)
+	{
+		Relation	mergingPartition = (Relation) lfirst(listptr);
+		TupleTableSlot *srcslot;
+		TupleConversionMap *tuple_map;
+		TableScanDesc scan;
+		Snapshot	snapshot;
+
+		/* Create tuple slot for new partition. */
+		srcslot = MakeSingleTupleTableSlot(RelationGetDescr(mergingPartition),
+										   table_slot_callbacks(mergingPartition));
+
+		/*
+		 * Map computing for moving attributes of merged partition to new
+		 * partition.
+		 */
+		tuple_map = convert_tuples_by_name(RelationGetDescr(mergingPartition),
+										   RelationGetDescr(newPartRel));
+
+		/* Scan through the rows. */
+		snapshot = RegisterSnapshot(GetLatestSnapshot());
+		scan = table_beginscan(mergingPartition, snapshot, 0, NULL);
+
+		while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot))
+		{
+			TupleTableSlot *insertslot;
+
+			/* Extract data from old tuple. */
+			slot_getallattrs(srcslot);
+
+			if (tuple_map)
+			{
+				/* Need to use map to copy attributes. */
+				insertslot = execute_attr_map_slot(tuple_map->attrMap, srcslot, dstslot);
+			}
+			else
+			{
+				/* Copy attributes directly. */
+				insertslot = dstslot;
+
+				ExecClearTuple(insertslot);
+
+				memcpy(insertslot->tts_values, srcslot->tts_values,
+					   sizeof(Datum) * srcslot->tts_nvalid);
+				memcpy(insertslot->tts_isnull, srcslot->tts_isnull,
+					   sizeof(bool) * srcslot->tts_nvalid);
+
+				ExecStoreVirtualTuple(insertslot);
+			}
+
+			/* Write the tuple out to the new relation. */
+			table_tuple_insert(newPartRel, insertslot, mycid,
+							   ti_options, bistate);
+
+			CHECK_FOR_INTERRUPTS();
+		}
+
+		table_endscan(scan);
+		UnregisterSnapshot(snapshot);
+
+		if (tuple_map)
+			free_conversion_map(tuple_map);
+
+		ExecDropSingleTupleTableSlot(srcslot);
+	}
+
+	ExecDropSingleTupleTableSlot(dstslot);
+	FreeBulkInsertState(bistate);
+
+	table_finish_bulk_insert(newPartRel, ti_options);
+}
+
+/*
+ * ALTER TABLE <name> MERGE PARTITIONS <partition-list> INTO <partition-name>
+ */
+static void
+ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
+					  PartitionCmd *cmd, AlterTableUtilityContext *context)
+{
+	Relation	newPartRel;
+	ListCell   *listptr;
+	List	   *mergingPartitionsList = NIL;
+	Oid			defaultPartOid;
+	Oid			namespaceId;
+	Oid			existingRelid;
+
+	/*
+	 * Lock all merged partitions, check them and create list with partitions
+	 * contexts.
+	 */
+	foreach(listptr, cmd->partlist)
+	{
+		RangeVar   *name = (RangeVar *) lfirst(listptr);
+		Relation	mergingPartition;
+
+		/*
+		 * We are going to detach and remove this partition: need to use
+		 * exclusive lock for preventing DML-queries to the partition.
+		 */
+		mergingPartition = table_openrv(name, AccessExclusiveLock);
+
+		/* Store a next merging partition into the list. */
+		mergingPartitionsList = lappend(mergingPartitionsList,
+										mergingPartition);
+	}
+
+	/*
+	 * Look up the namespace in which we are supposed to create the partition,
+	 * check we have permission to create there, lock it against concurrent
+	 * drop, and mark stmt->relation as RELPERSISTENCE_TEMP if a temporary
+	 * namespace is selected.
+	 */
+	cmd->name->relpersistence = rel->rd_rel->relpersistence;
+	namespaceId =
+		RangeVarGetAndCheckCreationNamespace(cmd->name, NoLock, NULL);
+
+	/*
+	 * Check if this name is already taken.  This helps us to detect the
+	 * situation when one of the merging partitions has the same name as the
+	 * new partition.  Otherwise, this would fail later on anyway but catching
+	 * this here allows us to emit a nicer error message.
+	 */
+	existingRelid = get_relname_relid(cmd->name->relname, namespaceId);
+
+	if (OidIsValid(existingRelid))
+	{
+		Relation	sameNamePartition = NULL;
+
+		foreach_ptr(RelationData, mergingPartition, mergingPartitionsList)
+		{
+			if (RelationGetRelid(mergingPartition) == existingRelid)
+			{
+				sameNamePartition = mergingPartition;
+				break;
+			}
+		}
+
+		if (sameNamePartition)
+		{
+			/*
+			 * The new partition has the same name as one of merging
+			 * partitions.
+			 */
+			char		tmpRelName[NAMEDATALEN];
+
+			/* Generate temporary name. */
+			sprintf(tmpRelName, "merge-%u-%X-tmp", RelationGetRelid(rel), MyProcPid);
+
+			/*
+			 * Rename the existing partition with a temporary name, leaving it
+			 * free for the new partition.  We don't need to care about this
+			 * in the future because we're going to eventually drop the
+			 * existing partition anyway.
+			 */
+			RenameRelationInternal(RelationGetRelid(sameNamePartition),
+								   tmpRelName, false, false);
+
+			/*
+			 * We must bump the command counter to make the new partition
+			 * tuple visible for rename.
+			 */
+			CommandCounterIncrement();
+		}
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_DUPLICATE_TABLE),
+					 errmsg("relation \"%s\" already exists", cmd->name->relname)));
+		}
+	}
+
+	/* Detach all merged partitions. */
+	defaultPartOid =
+		get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true));
+	foreach(listptr, mergingPartitionsList)
+	{
+		Relation	mergingPartition = (Relation) lfirst(listptr);
+
+		/* Remove the pg_inherits row first. */
+		RemoveInheritance(mergingPartition, rel, false);
+		/* Do the final part of detaching. */
+		DetachPartitionFinalize(rel, mergingPartition, false, defaultPartOid);
+	}
+
+	/* Create table for new partition, use partitioned table as model. */
+	newPartRel = createPartitionTable(cmd->name, rel, context);
+
+	/* Copy data from merged partitions to new partition. */
+	moveMergedTablesRows(rel, mergingPartitionsList, newPartRel);
+
+	/* Drop the current partitions before attaching the new one. */
+	foreach(listptr, mergingPartitionsList)
+	{
+		ObjectAddress object;
+		Relation	mergingPartition = (Relation) lfirst(listptr);
+
+		/* Get relation id before table_close() call. */
+		object.objectId = RelationGetRelid(mergingPartition);
+		object.classId = RelationRelationId;
+		object.objectSubId = 0;
+
+		/* Keep the lock until commit. */
+		table_close(mergingPartition, NoLock);
+
+		performDeletion(&object, DROP_RESTRICT, 0);
+	}
+	list_free(mergingPartitionsList);
+
+	/*
+	 * Attach a new partition to the partitioned table. wqueue = NULL:
+	 * verification for each cloned constraint is not needed.
+	 */
+	attachPartitionTable(NULL, rel, newPartRel, cmd->bound);
+
+	/* Keep the lock until commit. */
+	table_close(newPartRel, NoLock);
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 84cef57a70..d85c74cb7c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -760,7 +760,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	ORDER ORDINALITY OTHERS OUT_P OUTER_P
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
 
-	PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH
+	PARALLEL PARAMETER PARSER PARTIAL PARTITION PARTITIONS PASSING PASSWORD PATH
 	PLACING PLAN PLANS POLICY
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
 	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
@@ -2318,6 +2318,7 @@ partition_cmd:
 					n->subtype = AT_AttachPartition;
 					cmd->name = $3;
 					cmd->bound = $4;
+					cmd->partlist = NULL;
 					cmd->concurrent = false;
 					n->def = (Node *) cmd;
 
@@ -2332,6 +2333,7 @@ partition_cmd:
 					n->subtype = AT_DetachPartition;
 					cmd->name = $3;
 					cmd->bound = NULL;
+					cmd->partlist = NULL;
 					cmd->concurrent = $4;
 					n->def = (Node *) cmd;
 
@@ -2345,6 +2347,21 @@ partition_cmd:
 					n->subtype = AT_DetachPartitionFinalize;
 					cmd->name = $3;
 					cmd->bound = NULL;
+					cmd->partlist = NULL;
+					cmd->concurrent = false;
+					n->def = (Node *) cmd;
+					$$ = (Node *) n;
+				}
+			/* ALTER TABLE <name> MERGE PARTITIONS () INTO <partition_name> */
+			| MERGE PARTITIONS '(' qualified_name_list ')' INTO qualified_name
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					PartitionCmd *cmd = makeNode(PartitionCmd);
+
+					n->subtype = AT_MergePartitions;
+					cmd->name = $7;
+					cmd->bound = NULL;
+					cmd->partlist = $4;
 					cmd->concurrent = false;
 					n->def = (Node *) cmd;
 					$$ = (Node *) n;
@@ -2361,6 +2378,7 @@ index_partition_cmd:
 					n->subtype = AT_AttachPartition;
 					cmd->name = $3;
 					cmd->bound = NULL;
+					cmd->partlist = NULL;
 					cmd->concurrent = false;
 					n->def = (Node *) cmd;
 
@@ -17691,6 +17709,7 @@ unreserved_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PATH
@@ -18314,6 +18333,7 @@ bare_label_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PATH
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 79cad4ab30..63ea2ec5ea 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -32,6 +32,7 @@
 #include "catalog/heap.h"
 #include "catalog/index.h"
 #include "catalog/namespace.h"
+#include "catalog/partition.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
@@ -58,6 +59,8 @@
 #include "parser/parse_type.h"
 #include "parser/parse_utilcmd.h"
 #include "parser/parser.h"
+#include "partitioning/partdesc.h"
+#include "partitioning/partbounds.h"
 #include "rewrite/rewriteManip.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
@@ -3229,6 +3232,121 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
 }

+/*
+ * checkPartition
+ *		Check that partRelOid is an oid of partition of the parent table rel
+ */
+static void
+checkPartition(Relation rel, Oid partRelOid)
+{
+	Relation	partRel;
+
+	partRel = relation_open(partRelOid, AccessShareLock);
+
+	if (partRel->rd_rel->relkind != RELKIND_RELATION)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("\"%s\" is not a table",
+						RelationGetRelationName(partRel))));
+
+	if (!partRel->rd_rel->relispartition)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("\"%s\" is not a partition",
+						RelationGetRelationName(partRel))));
+
+	if (get_partition_parent(partRelOid, false) != RelationGetRelid(rel))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_TABLE),
+				 errmsg("relation \"%s\" is not a partition of relation \"%s\"",
+						RelationGetRelationName(partRel),
+						RelationGetRelationName(rel))));
+
+	/* Permissions checks */
+	if (!object_ownercheck(RelationRelationId, RelationGetRelid(partRel), GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(partRel->rd_rel->relkind),
+					   RelationGetRelationName(partRel));
+
+	relation_close(partRel, AccessShareLock);
+}
+
+/*
+ * transformPartitionCmdForMerge
+ *		Analyze the ALTER TABLE ... MERGE PARTITIONS command
+ *
+ * Does simple checks for merged partitions. Calculates bound of resulting
+ * partition.
+ */
+static void
+transformPartitionCmdForMerge(CreateStmtContext *cxt, PartitionCmd *partcmd)
+{
+	Oid			defaultPartOid;
+	Oid			partOid;
+	Relation	parent = cxt->rel;
+	PartitionKey key;
+	char		strategy;
+	ListCell   *listptr,
+			   *listptr2;
+	bool		isDefaultPart = false;
+	List	   *partOids = NIL;
+
+	if (parent->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("\"%s\" is not a partitioned table", RelationGetRelationName(parent))));
+
+	key = RelationGetPartitionKey(parent);
+	strategy = get_partition_strategy(key);
+
+	if (strategy == PARTITION_STRATEGY_HASH)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("partition of hash-partitioned table cannot be merged")));
+
+	/* Is current partition a DEFAULT partition? */
+	defaultPartOid = get_default_oid_from_partdesc(
+												   RelationGetPartitionDesc(parent, true));
+
+	foreach(listptr, partcmd->partlist)
+	{
+		RangeVar   *name = (RangeVar *) lfirst(listptr);
+
+		/* Partitions in the list should have different names. */
+		for_each_cell(listptr2, partcmd->partlist, lnext(partcmd->partlist, listptr))
+		{
+			RangeVar   *name2 = (RangeVar *) lfirst(listptr2);
+
+			if (equal(name, name2))
+				ereport(ERROR,
+						(errcode(ERRCODE_DUPLICATE_TABLE),
+						 errmsg("partition with name \"%s\" is already used", name->relname)),
+						parser_errposition(cxt->pstate, name2->location));
+		}
+
+		/* Search DEFAULT partition in the list. */
+		partOid = RangeVarGetRelid(name, NoLock, false);
+		if (partOid == defaultPartOid)
+			isDefaultPart = true;
+
+		checkPartition(parent, partOid);
+
+		partOids = lappend_oid(partOids, partOid);
+	}
+
+	/* Allocate bound of resulting partition. */
+	Assert(partcmd->bound == NULL);
+	partcmd->bound = makeNode(PartitionBoundSpec);
+
+	/* Fill partition bound. */
+	partcmd->bound->strategy = strategy;
+	partcmd->bound->location = -1;
+	partcmd->bound->is_default = isDefaultPart;
+	if (!isDefaultPart)
+		calculate_partition_bound_for_merge(parent, partcmd->partlist,
+											partOids, partcmd->bound,
+											cxt->pstate);
+}
+
 /*
  * transformAlterTableStmt -
  *		parse analysis for ALTER TABLE
@@ -3505,6 +3623,19 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 				newcmds = lappend(newcmds, cmd);
 				break;
 
+			case AT_MergePartitions:
+				{
+					PartitionCmd *partcmd = (PartitionCmd *) cmd->def;
+
+					if (list_length(partcmd->partlist) < 2)
+						ereport(ERROR,
+								(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+								 errmsg("list of new partitions should contain at least two items")));
+					transformPartitionCmdForMerge(&cxt, partcmd);
+					newcmds = lappend(newcmds, cmd);
+					break;
+				}
+
 			default:
 
 				/*
diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index c28639d2e3..6cb32d57ef 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -3214,8 +3214,9 @@ check_new_partition_bound(char *relname, Relation parent,
 						PartitionRangeDatum *datum;
 
 						/*
-						 * Point to problematic key in the lower datums list;
-						 * if we have equality, point to the first one.
+						 * Point to problematic key in the list of lower
+						 * datums; if we have equality, point to the first
+						 * one.
 						 */
 						datum = cmpval == 0 ? linitial(spec->lowerdatums) :
 							list_nth(spec->lowerdatums, abs(cmpval) - 1);
@@ -4977,3 +4978,210 @@ satisfies_hash_partition(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(rowHash % modulus == remainder);
 }
+
+/*
+ * check_two_partitions_bounds_range
+ *
+ * (function for BY RANGE partitioning)
+ *
+ * This is a helper function for check_partitions_for_split() and
+ * calculate_partition_bound_for_merge().
+ * This function compares upper bound of first_bound and lower bound of
+ * second_bound. These bounds should be equal except when
+ * "defaultPart == true" (this means that one of split partitions is DEFAULT).
+ * In this case upper bound of first_bound can be less than lower bound of
+ * second_bound because space between these bounds will be included in
+ * DEFAULT partition.
+ *
+ * parent:			partitioned table
+ * first_name:		name of first partition
+ * first_bound:		bound of first partition
+ * second_name:		name of second partition
+ * second_bound:	bound of second partition
+ * defaultPart:		true if one of split partitions is DEFAULT
+ * pstate:			pointer to ParseState struct for determining error position
+ */
+static void
+check_two_partitions_bounds_range(Relation parent,
+								  RangeVar *first_name,
+								  PartitionBoundSpec *first_bound,
+								  RangeVar *second_name,
+								  PartitionBoundSpec *second_bound,
+								  bool defaultPart,
+								  ParseState *pstate)
+{
+	PartitionKey key = RelationGetPartitionKey(parent);
+	PartitionRangeBound *first_upper;
+	PartitionRangeBound *second_lower;
+	int			cmpval;
+
+	Assert(key->strategy == PARTITION_STRATEGY_RANGE);
+
+	first_upper = make_one_partition_rbound(key, -1, first_bound->upperdatums, false);
+	second_lower = make_one_partition_rbound(key, -1, second_bound->lowerdatums, true);
+
+	/*
+	 * lower1=false (the second to last argument) for correct comparison of
+	 * lower and upper bounds.
+	 */
+	cmpval = partition_rbound_cmp(key->partnatts,
+								  key->partsupfunc,
+								  key->partcollation,
+								  second_lower->datums, second_lower->kind,
+								  false, first_upper);
+	if ((!defaultPart && cmpval) || (defaultPart && cmpval < 0))
+	{
+		PartitionRangeDatum *datum = linitial(second_bound->lowerdatums);
+
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("lower bound of partition \"%s\" conflicts with upper bound of previous partition \"%s\"",
+						second_name->relname, first_name->relname),
+				 parser_errposition(pstate, datum->location)));
+	}
+}
+
+/*
+ * get_partition_bound_spec
+ *
+ * Returns description of partition with Oid "partOid" and name "name".
+ */
+static PartitionBoundSpec *
+get_partition_bound_spec(Oid partOid, RangeVar *name)
+{
+	HeapTuple	tuple;
+	Datum		datum;
+	bool		isnull;
+	PartitionBoundSpec *boundspec = NULL;
+
+	/* Try fetching the tuple from the catcache, for speed. */
+	tuple = SearchSysCache1(RELOID, partOid);
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for relation \"%s\"",
+			 name->relname);
+
+	datum = SysCacheGetAttr(RELOID, tuple,
+							Anum_pg_class_relpartbound,
+							&isnull);
+	if (isnull)
+		elog(ERROR, "partition bound for relation \"%s\" is null",
+			 name->relname);
+
+	boundspec = stringToNode(TextDatumGetCString(datum));
+
+	if (!IsA(boundspec, PartitionBoundSpec))
+		elog(ERROR, "expected PartitionBoundSpec for relation \"%s\"",
+			 name->relname);
+
+	ReleaseSysCache(tuple);
+	return boundspec;
+}
+
+/*
+ * calculate_partition_bound_for_merge
+ *
+ * Calculates the bound of merged partition "spec" by using the bounds of
+ * partitions to be merged.
+ *
+ * parent:			partitioned table
+ * partNames:		names of partitions to be merged
+ * partOids:		Oids of partitions to be merged
+ * spec (out):		bounds specification of the merged partition
+ * pstate:			pointer to ParseState struct for determine error position
+ */
+void
+calculate_partition_bound_for_merge(Relation parent,
+									List *partNames,
+									List *partOids,
+									PartitionBoundSpec *spec,
+									ParseState *pstate)
+{
+	PartitionKey key = RelationGetPartitionKey(parent);
+	PartitionBoundSpec *bound;
+
+	Assert(!spec->is_default);
+
+	switch (key->strategy)
+	{
+		case PARTITION_STRATEGY_RANGE:
+			{
+				int			i;
+				PartitionRangeBound **lower_bounds;
+				int			nparts = list_length(partOids);
+				List	   *bounds = NIL;
+
+				lower_bounds = (PartitionRangeBound **)
+					palloc0(nparts * sizeof(PartitionRangeBound *));
+
+				/*
+				 * Create array of lower bounds and list of
+				 * PartitionBoundSpec.
+				 */
+				for (i = 0; i < nparts; i++)
+				{
+					bound = get_partition_bound_spec(list_nth_oid(partOids, i),
+													 (RangeVar *) list_nth(partNames, i));
+
+					lower_bounds[i] = make_one_partition_rbound(key, i, bound->lowerdatums, true);
+					bounds = lappend(bounds, bound);
+				}
+
+				/* Sort array of lower bounds. */
+				qsort_arg(lower_bounds, nparts, sizeof(PartitionRangeBound *),
+						  qsort_partition_rbound_cmp, (void *) key);
+
+				/* Ranges of partitions should not overlap. */
+				for (i = 1; i < nparts; i++)
+				{
+					int			index = lower_bounds[i]->index;
+					int			prev_index = lower_bounds[i - 1]->index;
+
+					check_two_partitions_bounds_range(parent,
+													  (RangeVar *) list_nth(partNames, prev_index),
+													  (PartitionBoundSpec *) list_nth(bounds, prev_index),
+													  (RangeVar *) list_nth(partNames, index),
+													  (PartitionBoundSpec *) list_nth(bounds, index),
+													  false, pstate);
+				}
+
+				/*
+				 * Lower bound of first partition is the lower bound of merged
+				 * partition.
+				 */
+				spec->lowerdatums =
+					((PartitionBoundSpec *) list_nth(bounds, lower_bounds[0]->index))->lowerdatums;
+
+				/*
+				 * Upper bound of last partition is the upper bound of merged
+				 * partition.
+				 */
+				spec->upperdatums =
+					((PartitionBoundSpec *) list_nth(bounds, lower_bounds[nparts - 1]->index))->upperdatums;
+
+				pfree(lower_bounds);
+				list_free(bounds);
+				break;
+			}
+
+		case PARTITION_STRATEGY_LIST:
+			{
+				ListCell   *listptr,
+						   *listptr2;
+
+				/* Consolidate bounds for all partitions in the list. */
+				forboth(listptr, partOids, listptr2, partNames)
+				{
+					RangeVar   *name = (RangeVar *) lfirst(listptr2);
+					Oid			curOid = lfirst_oid(listptr);
+
+					bound = get_partition_bound_spec(curOid, name);
+					spec->listdatums = list_concat(spec->listdatums, bound->listdatums);
+				}
+				break;
+			}
+
+		default:
+			elog(ERROR, "unexpected partition strategy: %d",
+				 (int) key->strategy);
+	}
+}
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index a7ccde6d7d..c3ea87fcb9 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -2353,6 +2353,7 @@ psql_completion(const char *text, int start, int end)
 					  "OWNER TO", "SET", "VALIDATE CONSTRAINT",
 					  "REPLICA IDENTITY", "ATTACH PARTITION",
 					  "DETACH PARTITION", "FORCE ROW LEVEL SECURITY",
+					  "MERGE PARTITIONS (",
 					  "OF", "NOT OF");
 	/* ALTER TABLE xxx ADD */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ADD"))
@@ -2620,6 +2621,15 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("ALTER", "TABLE", MatchAny, "DETACH", "PARTITION", MatchAny))
 		COMPLETE_WITH("CONCURRENTLY", "FINALIZE");
 
+	/* ALTER TABLE <name> MERGE PARTITIONS ( */
+	else if (Matches("ALTER", "TABLE", MatchAny, "MERGE", "PARTITIONS", "("))
+	{
+		set_completion_reference(prev4_wd);
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_partition_of_table);
+	}
+	else if (Matches("ALTER", "TABLE", MatchAny, "MERGE", "PARTITIONS", "(*)"))
+		COMPLETE_WITH("INTO");
+
 	/* ALTER TABLE <name> OF */
 	else if (Matches("ALTER", "TABLE", MatchAny, "OF"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_composite_datatypes);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 124d853e49..b6e7957ebb 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -937,6 +937,17 @@ typedef struct PartitionRangeDatum
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } PartitionRangeDatum;
 
+/*
+ * PartitionDesc - info about single partition for ALTER TABLE SPLIT PARTITION command
+ */
+typedef struct SinglePartitionSpec
+{
+	NodeTag		type;
+
+	RangeVar   *name;			/* name of partition */
+	PartitionBoundSpec *bound;	/* FOR VALUES, if attaching */
+} SinglePartitionSpec;
+
 /*
  * PartitionCmd - info for ALTER TABLE/INDEX ATTACH/DETACH PARTITION commands
  */
@@ -945,6 +956,8 @@ typedef struct PartitionCmd
 	NodeTag		type;
 	RangeVar   *name;			/* name of partition to attach/detach */
 	PartitionBoundSpec *bound;	/* FOR VALUES, if attaching */
+	List	   *partlist;		/* list of partitions, for MERGE/SPLIT
+								 * PARTITION command */
 	bool		concurrent;
 } PartitionCmd;
 
@@ -2400,6 +2413,7 @@ typedef enum AlterTableType
 	AT_AttachPartition,			/* ATTACH PARTITION */
 	AT_DetachPartition,			/* DETACH PARTITION */
 	AT_DetachPartitionFinalize, /* DETACH PARTITION FINALIZE */
+	AT_MergePartitions,			/* MERGE PARTITIONS */
 	AT_AddIdentity,				/* ADD IDENTITY */
 	AT_SetIdentity,				/* SET identity column options */
 	AT_DropIdentity,			/* DROP IDENTITY */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index f8659078ce..6b256fcece 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -335,6 +335,7 @@ PG_KEYWORD("parameter", PARAMETER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("parser", PARSER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("partitions", PARTITIONS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/partitioning/partbounds.h b/src/include/partitioning/partbounds.h
index 3d9cc1031f..0329d7bd84 100644
--- a/src/include/partitioning/partbounds.h
+++ b/src/include/partitioning/partbounds.h
@@ -143,4 +143,10 @@ extern int	partition_range_datum_bsearch(FmgrInfo *partsupfunc,
 extern int	partition_hash_bsearch(PartitionBoundInfo boundinfo,
 								   int modulus, int remainder);
 
+extern void calculate_partition_bound_for_merge(Relation parent,
+												List *partNames,
+												List *partOids,
+												PartitionBoundSpec *spec,
+												ParseState *pstate);
+
 #endif							/* PARTBOUNDS_H */
diff --git a/src/test/isolation/expected/partition-merge.out b/src/test/isolation/expected/partition-merge.out
new file mode 100644
index 0000000000..98446aaab5
--- /dev/null
+++ b/src/test/isolation/expected/partition-merge.out
@@ -0,0 +1,199 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s2b s2i s2c s1b s1merg s2b s2u s1c s2c s2s
+step s2b: BEGIN;
+step s2i: INSERT INTO tpart VALUES (1, 'text01');
+step s2c: COMMIT;
+step s1b: BEGIN;
+step s1merg: ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20;
+step s2b: BEGIN;
+step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; <waiting ...>
+step s1c: COMMIT;
+step s2u: <... completed>
+step s2c: COMMIT;
+step s2s: SELECT * FROM tpart;
+ i|t          
+--+-----------
+ 5|text05     
+15|text15     
+ 1|text01modif
+25|text25     
+35|text35     
+(5 rows)
+
+
+starting permutation: s2b s2i s2c s1brr s1merg s2b s2u s1c s2c s2s
+step s2b: BEGIN;
+step s2i: INSERT INTO tpart VALUES (1, 'text01');
+step s2c: COMMIT;
+step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1merg: ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20;
+step s2b: BEGIN;
+step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; <waiting ...>
+step s1c: COMMIT;
+step s2u: <... completed>
+step s2c: COMMIT;
+step s2s: SELECT * FROM tpart;
+ i|t          
+--+-----------
+ 5|text05     
+15|text15     
+ 1|text01modif
+25|text25     
+35|text35     
+(5 rows)
+
+
+starting permutation: s2b s2i s2c s1bs s1merg s2b s2u s1c s2c s2s
+step s2b: BEGIN;
+step s2i: INSERT INTO tpart VALUES (1, 'text01');
+step s2c: COMMIT;
+step s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1merg: ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20;
+step s2b: BEGIN;
+step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; <waiting ...>
+step s1c: COMMIT;
+step s2u: <... completed>
+step s2c: COMMIT;
+step s2s: SELECT * FROM tpart;
+ i|t          
+--+-----------
+ 5|text05     
+15|text15     
+ 1|text01modif
+25|text25     
+35|text35     
+(5 rows)
+
+
+starting permutation: s2brr s2i s2c s1b s1merg s2b s2u s1c s2c s2s
+step s2brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2i: INSERT INTO tpart VALUES (1, 'text01');
+step s2c: COMMIT;
+step s1b: BEGIN;
+step s1merg: ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20;
+step s2b: BEGIN;
+step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; <waiting ...>
+step s1c: COMMIT;
+step s2u: <... completed>
+step s2c: COMMIT;
+step s2s: SELECT * FROM tpart;
+ i|t          
+--+-----------
+ 5|text05     
+15|text15     
+ 1|text01modif
+25|text25     
+35|text35     
+(5 rows)
+
+
+starting permutation: s2brr s2i s2c s1brr s1merg s2b s2u s1c s2c s2s
+step s2brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2i: INSERT INTO tpart VALUES (1, 'text01');
+step s2c: COMMIT;
+step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1merg: ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20;
+step s2b: BEGIN;
+step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; <waiting ...>
+step s1c: COMMIT;
+step s2u: <... completed>
+step s2c: COMMIT;
+step s2s: SELECT * FROM tpart;
+ i|t          
+--+-----------
+ 5|text05     
+15|text15     
+ 1|text01modif
+25|text25     
+35|text35     
+(5 rows)
+
+
+starting permutation: s2brr s2i s2c s1bs s1merg s2b s2u s1c s2c s2s
+step s2brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2i: INSERT INTO tpart VALUES (1, 'text01');
+step s2c: COMMIT;
+step s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1merg: ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20;
+step s2b: BEGIN;
+step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; <waiting ...>
+step s1c: COMMIT;
+step s2u: <... completed>
+step s2c: COMMIT;
+step s2s: SELECT * FROM tpart;
+ i|t          
+--+-----------
+ 5|text05     
+15|text15     
+ 1|text01modif
+25|text25     
+35|text35     
+(5 rows)
+
+
+starting permutation: s2bs s2i s2c s1b s1merg s2b s2u s1c s2c s2s
+step s2bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2i: INSERT INTO tpart VALUES (1, 'text01');
+step s2c: COMMIT;
+step s1b: BEGIN;
+step s1merg: ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20;
+step s2b: BEGIN;
+step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; <waiting ...>
+step s1c: COMMIT;
+step s2u: <... completed>
+step s2c: COMMIT;
+step s2s: SELECT * FROM tpart;
+ i|t          
+--+-----------
+ 5|text05     
+15|text15     
+ 1|text01modif
+25|text25     
+35|text35     
+(5 rows)
+
+
+starting permutation: s2bs s2i s2c s1brr s1merg s2b s2u s1c s2c s2s
+step s2bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2i: INSERT INTO tpart VALUES (1, 'text01');
+step s2c: COMMIT;
+step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1merg: ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20;
+step s2b: BEGIN;
+step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; <waiting ...>
+step s1c: COMMIT;
+step s2u: <... completed>
+step s2c: COMMIT;
+step s2s: SELECT * FROM tpart;
+ i|t          
+--+-----------
+ 5|text05     
+15|text15     
+ 1|text01modif
+25|text25     
+35|text35     
+(5 rows)
+
+
+starting permutation: s2bs s2i s2c s1bs s1merg s2b s2u s1c s2c s2s
+step s2bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2i: INSERT INTO tpart VALUES (1, 'text01');
+step s2c: COMMIT;
+step s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1merg: ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20;
+step s2b: BEGIN;
+step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; <waiting ...>
+step s1c: COMMIT;
+step s2u: <... completed>
+step s2c: COMMIT;
+step s2s: SELECT * FROM tpart;
+ i|t          
+--+-----------
+ 5|text05     
+15|text15     
+ 1|text01modif
+25|text25     
+35|text35     
+(5 rows)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 143109aa4d..c1499338d6 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -106,6 +106,7 @@ test: partition-key-update-1
 test: partition-key-update-2
 test: partition-key-update-3
 test: partition-key-update-4
+test: partition-merge
 test: plpgsql-toast
 test: cluster-conflict
 test: cluster-conflict-partition
diff --git a/src/test/isolation/specs/partition-merge.spec b/src/test/isolation/specs/partition-merge.spec
new file mode 100644
index 0000000000..dc2b9d3445
--- /dev/null
+++ b/src/test/isolation/specs/partition-merge.spec
@@ -0,0 +1,54 @@
+# Verify that MERGE operation locks DML operations with partitioned table
+
+setup
+{
+  DROP TABLE IF EXISTS tpart;
+  CREATE TABLE tpart(i int, t text) partition by range(i);
+  CREATE TABLE tpart_00_10 PARTITION OF tpart FOR VALUES FROM (0) TO (10);
+  CREATE TABLE tpart_10_20 PARTITION OF tpart FOR VALUES FROM (10) TO (20);
+  CREATE TABLE tpart_20_30 PARTITION OF tpart FOR VALUES FROM (20) TO (30);
+  CREATE TABLE tpart_default PARTITION OF tpart DEFAULT;
+  INSERT INTO tpart VALUES (5, 'text05');
+  INSERT INTO tpart VALUES (15, 'text15');
+  INSERT INTO tpart VALUES (25, 'text25');
+  INSERT INTO tpart VALUES (35, 'text35');
+}
+
+teardown
+{
+  DROP TABLE tpart;
+}
+
+session s1
+step s1b	{ BEGIN; }
+step s1brr	{ BEGIN ISOLATION LEVEL REPEATABLE READ; }
+step s1bs	{ BEGIN ISOLATION LEVEL SERIALIZABLE; }
+step s1merg	{ ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20; }
+step s1c	{ COMMIT; }
+
+
+session s2
+step s2b	{ BEGIN; }
+step s2brr	{ BEGIN ISOLATION LEVEL REPEATABLE READ; }
+step s2bs	{ BEGIN ISOLATION LEVEL SERIALIZABLE; }
+step s2i	{ INSERT INTO tpart VALUES (1, 'text01'); }
+step s2u	{ UPDATE tpart SET t = 'text01modif' where i = 1; }
+step s2c	{ COMMIT; }
+step s2s	{ SELECT * FROM tpart; }
+
+
+# s2 inserts row into table. s1 starts MERGE PARTITIONS then
+# s2 is trying to update inserted row and waits until s1 finishes
+# MERGE operation.
+
+permutation s2b s2i s2c s1b s1merg s2b s2u s1c s2c s2s
+permutation s2b s2i s2c s1brr s1merg s2b s2u s1c s2c s2s
+permutation s2b s2i s2c s1bs s1merg s2b s2u s1c s2c s2s
+
+permutation s2brr s2i s2c s1b s1merg s2b s2u s1c s2c s2s
+permutation s2brr s2i s2c s1brr s1merg s2b s2u s1c s2c s2s
+permutation s2brr s2i s2c s1bs s1merg s2b s2u s1c s2c s2s
+
+permutation s2bs s2i s2c s1b s1merg s2b s2u s1c s2c s2s
+permutation s2bs s2i s2c s1brr s1merg s2b s2u s1c s2c s2s
+permutation s2bs s2i s2c s1bs s1merg s2b s2u s1c s2c s2s
diff --git a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
index 2758ae82d7..cc341afdf4 100644
--- a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
+++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
@@ -300,6 +300,9 @@ get_altertable_subcmdinfo(PG_FUNCTION_ARGS)
 			case AT_DetachPartitionFinalize:
 				strtype = "DETACH PARTITION ... FINALIZE";
 				break;
+			case AT_MergePartitions:
+				strtype = "MERGE PARTITIONS";
+				break;
 			case AT_AddIdentity:
 				strtype = "ADD IDENTITY";
 				break;
diff --git a/src/test/regress/expected/partition_merge.out b/src/test/regress/expected/partition_merge.out
new file mode 100644
index 0000000000..59836e2d35
--- /dev/null
+++ b/src/test/regress/expected/partition_merge.out
@@ -0,0 +1,945 @@
+--
+-- PARTITIONS_MERGE
+-- Tests for "ALTER TABLE ... MERGE PARTITIONS ..." command
+--
+CREATE SCHEMA partitions_merge_schema;
+CREATE SCHEMA partitions_merge_schema2;
+SET search_path = partitions_merge_schema, public;
+--
+-- BY RANGE partitioning
+--
+--
+-- Test for error codes
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_dec2021 PARTITION OF sales_range FOR VALUES FROM ('2021-12-01') TO ('2021-12-31');
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-03-01');
+CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM ('2022-03-01') TO ('2022-04-01');
+CREATE TABLE sales_apr2022 (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_apr_1 PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-04-15');
+CREATE TABLE sales_apr_2 PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-15') TO ('2022-05-01');
+ALTER TABLE sales_range ATTACH PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01');
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+-- ERROR:  partition with name "sales_feb2022" is already used
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_feb2022) INTO sales_feb_mar_apr2022;
+ERROR:  partition with name "sales_feb2022" is already used
+LINE 1: ...e MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_feb2...
+                                                             ^
+-- ERROR:  "sales_apr2022" is not a table
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_apr2022) INTO sales_feb_mar_apr2022;
+ERROR:  "sales_apr2022" is not a table
+-- ERROR:  lower bound of partition "sales_mar2022" conflicts with upper bound of previous partition "sales_jan2022"
+-- (space between sections sales_jan2022 and sales_mar2022)
+ALTER TABLE sales_range MERGE PARTITIONS (sales_jan2022, sales_mar2022) INTO sales_jan_mar2022;
+ERROR:  lower bound of partition "sales_mar2022" conflicts with upper bound of previous partition "sales_jan2022"
+-- ERROR:  lower bound of partition "sales_jan2022" conflicts with upper bound of previous partition "sales_dec2021"
+-- (space between sections sales_dec2021 and sales_jan2022)
+ALTER TABLE sales_range MERGE PARTITIONS (sales_dec2021, sales_jan2022, sales_feb2022) INTO sales_dec_jan_feb2022;
+ERROR:  lower bound of partition "sales_jan2022" conflicts with upper bound of previous partition "sales_dec2021"
+-- NO ERROR: test for custom partitions order, source partitions not in the search_path
+SET search_path = partitions_merge_schema2, public;
+ALTER TABLE partitions_merge_schema.sales_range MERGE PARTITIONS (
+  partitions_merge_schema.sales_feb2022,
+  partitions_merge_schema.sales_mar2022,
+  partitions_merge_schema.sales_jan2022) INTO sales_jan_feb_mar2022;
+SET search_path = partitions_merge_schema, public;
+SELECT c.oid::pg_catalog.regclass, c.relkind, inhdetachpending, pg_catalog.pg_get_expr(c.relpartbound, c.oid)
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 'sales_range'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+                      oid                       | relkind | inhdetachpending |                   pg_get_expr                    
+------------------------------------------------+---------+------------------+--------------------------------------------------
+ partitions_merge_schema2.sales_jan_feb_mar2022 | r       | f                | FOR VALUES FROM ('01-01-2022') TO ('04-01-2022')
+ sales_apr2022                                  | p       | f                | FOR VALUES FROM ('04-01-2022') TO ('05-01-2022')
+ sales_dec2021                                  | r       | f                | FOR VALUES FROM ('12-01-2021') TO ('12-31-2021')
+ sales_others                                   | r       | f                | DEFAULT
+(4 rows)
+
+DROP TABLE sales_range;
+--
+-- Add rows into partitioned table, then merge partitions
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-03-01');
+CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM ('2022-03-01') TO ('2022-04-01');
+CREATE TABLE sales_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-04-01') TO ('2022-05-01');
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree (sales_date);
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-10');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
+INSERT INTO sales_range VALUES (4,  'Ivanov',   750,  '2022-04-13');
+INSERT INTO sales_range VALUES (5,  'Deev',     250,  '2022-04-07');
+INSERT INTO sales_range VALUES (6,  'Poirot',   150,  '2022-02-11');
+INSERT INTO sales_range VALUES (7,  'Li',       175,  '2022-03-08');
+INSERT INTO sales_range VALUES (8,  'Ericsson', 185,  '2022-02-23');
+INSERT INTO sales_range VALUES (9,  'Muller',   250,  '2022-03-11');
+INSERT INTO sales_range VALUES (10, 'Halder',   350,  '2022-01-28');
+INSERT INTO sales_range VALUES (11, 'Trump',    380,  '2022-04-06');
+INSERT INTO sales_range VALUES (12, 'Plato',    350,  '2022-03-19');
+INSERT INTO sales_range VALUES (13, 'Gandi',    377,  '2022-01-09');
+INSERT INTO sales_range VALUES (14, 'Smith',    510,  '2022-05-04');
+SELECT pg_catalog.pg_get_partkeydef('sales_range'::regclass);
+ pg_get_partkeydef  
+--------------------
+ RANGE (sales_date)
+(1 row)
+
+-- show partitions with conditions:
+SELECT c.oid::pg_catalog.regclass, c.relkind, inhdetachpending, pg_catalog.pg_get_expr(c.relpartbound, c.oid)
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 'sales_range'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+      oid      | relkind | inhdetachpending |                   pg_get_expr                    
+---------------+---------+------------------+--------------------------------------------------
+ sales_apr2022 | r       | f                | FOR VALUES FROM ('04-01-2022') TO ('05-01-2022')
+ sales_feb2022 | r       | f                | FOR VALUES FROM ('02-01-2022') TO ('03-01-2022')
+ sales_jan2022 | r       | f                | FOR VALUES FROM ('01-01-2022') TO ('02-01-2022')
+ sales_mar2022 | r       | f                | FOR VALUES FROM ('03-01-2022') TO ('04-01-2022')
+ sales_others  | r       | f                | DEFAULT
+(5 rows)
+
+-- check schema-qualified name of the new partition
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_apr2022) INTO partitions_merge_schema2.sales_feb_mar_apr2022;
+-- show partitions with conditions:
+SELECT c.oid::pg_catalog.regclass, c.relkind, inhdetachpending, pg_catalog.pg_get_expr(c.relpartbound, c.oid)
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 'sales_range'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+                      oid                       | relkind | inhdetachpending |                   pg_get_expr                    
+------------------------------------------------+---------+------------------+--------------------------------------------------
+ partitions_merge_schema2.sales_feb_mar_apr2022 | r       | f                | FOR VALUES FROM ('02-01-2022') TO ('05-01-2022')
+ sales_jan2022                                  | r       | f                | FOR VALUES FROM ('01-01-2022') TO ('02-01-2022')
+ sales_others                                   | r       | f                | DEFAULT
+(3 rows)
+
+SELECT * FROM pg_indexes WHERE tablename = 'sales_feb_mar_apr2022' and schemaname = 'partitions_merge_schema2';
+        schemaname        |       tablename       |              indexname               | tablespace |                                                           indexdef                                                           
+--------------------------+-----------------------+--------------------------------------+------------+------------------------------------------------------------------------------------------------------------------------------
+ partitions_merge_schema2 | sales_feb_mar_apr2022 | sales_feb_mar_apr2022_sales_date_idx |            | CREATE INDEX sales_feb_mar_apr2022_sales_date_idx ON partitions_merge_schema2.sales_feb_mar_apr2022 USING btree (sales_date)
+(1 row)
+
+SELECT * FROM sales_range;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              1 | May              |         1000 | 01-31-2022
+             10 | Halder           |          350 | 01-28-2022
+             13 | Gandi            |          377 | 01-09-2022
+              2 | Smirnoff         |          500 | 02-10-2022
+              6 | Poirot           |          150 | 02-11-2022
+              8 | Ericsson         |          185 | 02-23-2022
+              7 | Li               |          175 | 03-08-2022
+              9 | Muller           |          250 | 03-11-2022
+             12 | Plato            |          350 | 03-19-2022
+              3 | Ford             |         2000 | 04-30-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+             14 | Smith            |          510 | 05-04-2022
+(14 rows)
+
+SELECT * FROM sales_jan2022;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              1 | May              |         1000 | 01-31-2022
+             10 | Halder           |          350 | 01-28-2022
+             13 | Gandi            |          377 | 01-09-2022
+(3 rows)
+
+SELECT * FROM partitions_merge_schema2.sales_feb_mar_apr2022;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              2 | Smirnoff         |          500 | 02-10-2022
+              6 | Poirot           |          150 | 02-11-2022
+              8 | Ericsson         |          185 | 02-23-2022
+              7 | Li               |          175 | 03-08-2022
+              9 | Muller           |          250 | 03-11-2022
+             12 | Plato            |          350 | 03-19-2022
+              3 | Ford             |         2000 | 04-30-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+(10 rows)
+
+SELECT * FROM sales_others;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+             14 | Smith            |          510 | 05-04-2022
+(1 row)
+
+-- Use indexscan for testing indexes
+SET enable_seqscan = OFF;
+SELECT * FROM partitions_merge_schema2.sales_feb_mar_apr2022 where sales_date > '2022-01-01';
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              2 | Smirnoff         |          500 | 02-10-2022
+              6 | Poirot           |          150 | 02-11-2022
+              8 | Ericsson         |          185 | 02-23-2022
+              7 | Li               |          175 | 03-08-2022
+              9 | Muller           |          250 | 03-11-2022
+             12 | Plato            |          350 | 03-19-2022
+             11 | Trump            |          380 | 04-06-2022
+              5 | Deev             |          250 | 04-07-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              3 | Ford             |         2000 | 04-30-2022
+(10 rows)
+
+RESET enable_seqscan;
+DROP TABLE sales_range;
+--
+-- Merge some partitions into DEFAULT partition
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-03-01');
+CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM ('2022-03-01') TO ('2022-04-01');
+CREATE TABLE sales_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-04-01') TO ('2022-05-01');
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree (sales_date);
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-10');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
+INSERT INTO sales_range VALUES (4,  'Ivanov',   750,  '2022-04-13');
+INSERT INTO sales_range VALUES (5,  'Deev',     250,  '2022-04-07');
+INSERT INTO sales_range VALUES (6,  'Poirot',   150,  '2022-02-11');
+INSERT INTO sales_range VALUES (7,  'Li',       175,  '2022-03-08');
+INSERT INTO sales_range VALUES (8,  'Ericsson', 185,  '2022-02-23');
+INSERT INTO sales_range VALUES (9,  'Muller',   250,  '2022-03-11');
+INSERT INTO sales_range VALUES (10, 'Halder',   350,  '2022-01-28');
+INSERT INTO sales_range VALUES (11, 'Trump',    380,  '2022-04-06');
+INSERT INTO sales_range VALUES (12, 'Plato',    350,  '2022-03-19');
+INSERT INTO sales_range VALUES (13, 'Gandi',    377,  '2022-01-09');
+INSERT INTO sales_range VALUES (14, 'Smith',    510,  '2022-05-04');
+-- Merge partitions (include DEFAULT partition) into partition with the same
+-- name
+ALTER TABLE sales_range MERGE PARTITIONS
+  (sales_jan2022, sales_mar2022, partitions_merge_schema.sales_others) INTO sales_others;
+select * from sales_others;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              1 | May              |         1000 | 01-31-2022
+             10 | Halder           |          350 | 01-28-2022
+             13 | Gandi            |          377 | 01-09-2022
+              7 | Li               |          175 | 03-08-2022
+              9 | Muller           |          250 | 03-11-2022
+             12 | Plato            |          350 | 03-19-2022
+             14 | Smith            |          510 | 05-04-2022
+(7 rows)
+
+-- show partitions with conditions:
+SELECT c.oid::pg_catalog.regclass, c.relkind, inhdetachpending, pg_catalog.pg_get_expr(c.relpartbound, c.oid)
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 'sales_range'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+      oid      | relkind | inhdetachpending |                   pg_get_expr                    
+---------------+---------+------------------+--------------------------------------------------
+ sales_apr2022 | r       | f                | FOR VALUES FROM ('04-01-2022') TO ('05-01-2022')
+ sales_feb2022 | r       | f                | FOR VALUES FROM ('02-01-2022') TO ('03-01-2022')
+ sales_others  | r       | f                | DEFAULT
+(3 rows)
+
+DROP TABLE sales_range;
+--
+-- Test for:
+--   * composite partition key;
+--   * GENERATED column;
+--   * column with DEFAULT value.
+--
+CREATE TABLE sales_date (salesperson_name VARCHAR(30), sales_year INT, sales_month INT, sales_day INT,
+  sales_date VARCHAR(10) GENERATED ALWAYS AS
+    (LPAD(sales_year::text, 4, '0') || '.' || LPAD(sales_month::text, 2, '0') || '.' || LPAD(sales_day::text, 2, '0')) STORED,
+  sales_department VARCHAR(30) DEFAULT 'Sales department')
+  PARTITION BY RANGE (sales_year, sales_month, sales_day);
+CREATE TABLE sales_dec2022 PARTITION OF sales_date FOR VALUES FROM (2021, 12, 1) TO (2022, 1, 1);
+CREATE TABLE sales_jan2022 PARTITION OF sales_date FOR VALUES FROM (2022, 1, 1) TO (2022, 2, 1);
+CREATE TABLE sales_feb2022 PARTITION OF sales_date FOR VALUES FROM (2022, 2, 1) TO (2022, 3, 1);
+CREATE TABLE sales_other PARTITION OF sales_date FOR VALUES FROM (2022, 3, 1) TO (MAXVALUE, MAXVALUE, MAXVALUE);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2021, 12, 7);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2021, 12, 8);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 1, 1);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 2, 4);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 1, 2);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 2, 1);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 3, 3);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 3, 4);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 5, 1);
+SELECT * FROM sales_date;
+ salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department 
+------------------+------------+-------------+-----------+------------+------------------
+ Manager1         |       2021 |          12 |         7 | 2021.12.07 | Sales department
+ Manager2         |       2021 |          12 |         8 | 2021.12.08 | Sales department
+ Manager3         |       2022 |           1 |         1 | 2022.01.01 | Sales department
+ Manager2         |       2022 |           1 |         2 | 2022.01.02 | Sales department
+ Manager1         |       2022 |           2 |         4 | 2022.02.04 | Sales department
+ Manager3         |       2022 |           2 |         1 | 2022.02.01 | Sales department
+ Manager1         |       2022 |           3 |         3 | 2022.03.03 | Sales department
+ Manager2         |       2022 |           3 |         4 | 2022.03.04 | Sales department
+ Manager3         |       2022 |           5 |         1 | 2022.05.01 | Sales department
+(9 rows)
+
+SELECT * FROM sales_dec2022;
+ salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department 
+------------------+------------+-------------+-----------+------------+------------------
+ Manager1         |       2021 |          12 |         7 | 2021.12.07 | Sales department
+ Manager2         |       2021 |          12 |         8 | 2021.12.08 | Sales department
+(2 rows)
+
+SELECT * FROM sales_jan2022;
+ salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department 
+------------------+------------+-------------+-----------+------------+------------------
+ Manager3         |       2022 |           1 |         1 | 2022.01.01 | Sales department
+ Manager2         |       2022 |           1 |         2 | 2022.01.02 | Sales department
+(2 rows)
+
+SELECT * FROM sales_feb2022;
+ salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department 
+------------------+------------+-------------+-----------+------------+------------------
+ Manager1         |       2022 |           2 |         4 | 2022.02.04 | Sales department
+ Manager3         |       2022 |           2 |         1 | 2022.02.01 | Sales department
+(2 rows)
+
+SELECT * FROM sales_other;
+ salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department 
+------------------+------------+-------------+-----------+------------+------------------
+ Manager1         |       2022 |           3 |         3 | 2022.03.03 | Sales department
+ Manager2         |       2022 |           3 |         4 | 2022.03.04 | Sales department
+ Manager3         |       2022 |           5 |         1 | 2022.05.01 | Sales department
+(3 rows)
+
+ALTER TABLE sales_date MERGE PARTITIONS (sales_jan2022, sales_feb2022) INTO sales_jan_feb2022;
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 1, 10);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 2, 10);
+SELECT * FROM sales_date;
+ salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department 
+------------------+------------+-------------+-----------+------------+------------------
+ Manager1         |       2021 |          12 |         7 | 2021.12.07 | Sales department
+ Manager2         |       2021 |          12 |         8 | 2021.12.08 | Sales department
+ Manager3         |       2022 |           1 |         1 | 2022.01.01 | Sales department
+ Manager2         |       2022 |           1 |         2 | 2022.01.02 | Sales department
+ Manager1         |       2022 |           2 |         4 | 2022.02.04 | Sales department
+ Manager3         |       2022 |           2 |         1 | 2022.02.01 | Sales department
+ Manager1         |       2022 |           1 |        10 | 2022.01.10 | Sales department
+ Manager2         |       2022 |           2 |        10 | 2022.02.10 | Sales department
+ Manager1         |       2022 |           3 |         3 | 2022.03.03 | Sales department
+ Manager2         |       2022 |           3 |         4 | 2022.03.04 | Sales department
+ Manager3         |       2022 |           5 |         1 | 2022.05.01 | Sales department
+(11 rows)
+
+SELECT * FROM sales_dec2022;
+ salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department 
+------------------+------------+-------------+-----------+------------+------------------
+ Manager1         |       2021 |          12 |         7 | 2021.12.07 | Sales department
+ Manager2         |       2021 |          12 |         8 | 2021.12.08 | Sales department
+(2 rows)
+
+SELECT * FROM sales_jan_feb2022;
+ salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department 
+------------------+------------+-------------+-----------+------------+------------------
+ Manager3         |       2022 |           1 |         1 | 2022.01.01 | Sales department
+ Manager2         |       2022 |           1 |         2 | 2022.01.02 | Sales department
+ Manager1         |       2022 |           2 |         4 | 2022.02.04 | Sales department
+ Manager3         |       2022 |           2 |         1 | 2022.02.01 | Sales department
+ Manager1         |       2022 |           1 |        10 | 2022.01.10 | Sales department
+ Manager2         |       2022 |           2 |        10 | 2022.02.10 | Sales department
+(6 rows)
+
+SELECT * FROM sales_other;
+ salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department 
+------------------+------------+-------------+-----------+------------+------------------
+ Manager1         |       2022 |           3 |         3 | 2022.03.03 | Sales department
+ Manager2         |       2022 |           3 |         4 | 2022.03.04 | Sales department
+ Manager3         |       2022 |           5 |         1 | 2022.05.01 | Sales department
+(3 rows)
+
+DROP TABLE sales_date;
+--
+-- Test: merge partitions of partitioned table with triggers
+--
+CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id);
+CREATE TABLE salespeople01_10 PARTITION OF salespeople FOR VALUES FROM (1) TO (10);
+CREATE TABLE salespeople10_20 PARTITION OF salespeople FOR VALUES FROM (10) TO (20);
+CREATE TABLE salespeople20_30 PARTITION OF salespeople FOR VALUES FROM (20) TO (30);
+CREATE TABLE salespeople30_40 PARTITION OF salespeople FOR VALUES FROM (30) TO (40);
+INSERT INTO salespeople VALUES (1,  'Poirot');
+CREATE OR REPLACE FUNCTION after_insert_row_trigger() RETURNS trigger LANGUAGE 'plpgsql' AS $BODY$
+BEGIN
+	RAISE NOTICE 'trigger(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
+    RETURN NULL;
+END;
+$BODY$;
+CREATE TRIGGER salespeople_after_insert_statement_trigger
+    AFTER INSERT
+    ON salespeople
+    FOR EACH STATEMENT
+    EXECUTE PROCEDURE after_insert_row_trigger('salespeople');
+CREATE TRIGGER salespeople_after_insert_row_trigger
+    AFTER INSERT
+    ON salespeople
+    FOR EACH ROW
+    EXECUTE PROCEDURE after_insert_row_trigger('salespeople');
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salespeople VALUES (10, 'May');
+NOTICE:  trigger(salespeople) called: action = INSERT, when = AFTER, level = ROW
+NOTICE:  trigger(salespeople) called: action = INSERT, when = AFTER, level = STATEMENT
+-- 1 trigger should fire here (row):
+INSERT INTO salespeople10_20 VALUES (19, 'Ivanov');
+NOTICE:  trigger(salespeople) called: action = INSERT, when = AFTER, level = ROW
+ALTER TABLE salespeople MERGE PARTITIONS (salespeople10_20, salespeople20_30, salespeople30_40) INTO salespeople10_40;
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salespeople VALUES (20, 'Smirnoff');
+NOTICE:  trigger(salespeople) called: action = INSERT, when = AFTER, level = ROW
+NOTICE:  trigger(salespeople) called: action = INSERT, when = AFTER, level = STATEMENT
+-- 1 trigger should fire here (row):
+INSERT INTO salespeople10_40 VALUES (30, 'Ford');
+NOTICE:  trigger(salespeople) called: action = INSERT, when = AFTER, level = ROW
+SELECT * FROM salespeople01_10;
+ salesperson_id | salesperson_name 
+----------------+------------------
+              1 | Poirot
+(1 row)
+
+SELECT * FROM salespeople10_40;
+ salesperson_id | salesperson_name 
+----------------+------------------
+             10 | May
+             19 | Ivanov
+             20 | Smirnoff
+             30 | Ford
+(4 rows)
+
+DROP TABLE salespeople;
+DROP FUNCTION after_insert_row_trigger();
+--
+-- Test: merge partitions with deleted columns
+--
+CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id);
+CREATE TABLE salespeople01_10 PARTITION OF salespeople FOR VALUES FROM (1) TO (10);
+-- Create partitions with some deleted columns:
+CREATE TABLE salespeople10_20(d1 VARCHAR(30), salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30));
+CREATE TABLE salespeople20_30(salesperson_id INT PRIMARY KEY, d2 INT,  salesperson_name VARCHAR(30));
+CREATE TABLE salespeople30_40(salesperson_id INT PRIMARY KEY, d3 DATE, salesperson_name VARCHAR(30));
+INSERT INTO salespeople10_20 VALUES ('dummy value 1', 19, 'Ivanov');
+INSERT INTO salespeople20_30 VALUES (20, 101, 'Smirnoff');
+INSERT INTO salespeople30_40 VALUES (31, now(), 'Popov');
+ALTER TABLE salespeople10_20 DROP COLUMN d1;
+ALTER TABLE salespeople20_30 DROP COLUMN d2;
+ALTER TABLE salespeople30_40 DROP COLUMN d3;
+ALTER TABLE salespeople ATTACH PARTITION salespeople10_20 FOR VALUES FROM (10) TO (20);
+ALTER TABLE salespeople ATTACH PARTITION salespeople20_30 FOR VALUES FROM (20) TO (30);
+ALTER TABLE salespeople ATTACH PARTITION salespeople30_40 FOR VALUES FROM (30) TO (40);
+INSERT INTO salespeople VALUES (1, 'Poirot');
+INSERT INTO salespeople VALUES (10, 'May');
+INSERT INTO salespeople VALUES (30, 'Ford');
+ALTER TABLE salespeople MERGE PARTITIONS (salespeople10_20, salespeople20_30, salespeople30_40) INTO salespeople10_40;
+select * from salespeople;
+ salesperson_id | salesperson_name 
+----------------+------------------
+              1 | Poirot
+             19 | Ivanov
+             10 | May
+             20 | Smirnoff
+             31 | Popov
+             30 | Ford
+(6 rows)
+
+select * from salespeople01_10;
+ salesperson_id | salesperson_name 
+----------------+------------------
+              1 | Poirot
+(1 row)
+
+select * from salespeople10_40;
+ salesperson_id | salesperson_name 
+----------------+------------------
+             19 | Ivanov
+             10 | May
+             20 | Smirnoff
+             31 | Popov
+             30 | Ford
+(5 rows)
+
+DROP TABLE salespeople;
+--
+-- Test: merge sub-partitions
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-03-01');
+CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM ('2022-03-01') TO ('2022-04-01');
+CREATE TABLE sales_apr2022 (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_apr2022_01_10 PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-04-10');
+CREATE TABLE sales_apr2022_10_20 PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-10') TO ('2022-04-20');
+CREATE TABLE sales_apr2022_20_30 PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-20') TO ('2022-05-01');
+ALTER TABLE sales_range ATTACH PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01');
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree (sales_date);
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-10');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
+INSERT INTO sales_range VALUES (4,  'Ivanov',   750,  '2022-04-13');
+INSERT INTO sales_range VALUES (5,  'Deev',     250,  '2022-04-07');
+INSERT INTO sales_range VALUES (6,  'Poirot',   150,  '2022-02-11');
+INSERT INTO sales_range VALUES (7,  'Li',       175,  '2022-03-08');
+INSERT INTO sales_range VALUES (8,  'Ericsson', 185,  '2022-02-23');
+INSERT INTO sales_range VALUES (9,  'Muller',   250,  '2022-03-11');
+INSERT INTO sales_range VALUES (10, 'Halder',   350,  '2022-01-28');
+INSERT INTO sales_range VALUES (11, 'Trump',    380,  '2022-04-06');
+INSERT INTO sales_range VALUES (12, 'Plato',    350,  '2022-03-19');
+INSERT INTO sales_range VALUES (13, 'Gandi',    377,  '2022-01-09');
+INSERT INTO sales_range VALUES (14, 'Smith',    510,  '2022-05-04');
+SELECT * FROM sales_range;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              1 | May              |         1000 | 01-31-2022
+             10 | Halder           |          350 | 01-28-2022
+             13 | Gandi            |          377 | 01-09-2022
+              2 | Smirnoff         |          500 | 02-10-2022
+              6 | Poirot           |          150 | 02-11-2022
+              8 | Ericsson         |          185 | 02-23-2022
+              7 | Li               |          175 | 03-08-2022
+              9 | Muller           |          250 | 03-11-2022
+             12 | Plato            |          350 | 03-19-2022
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              3 | Ford             |         2000 | 04-30-2022
+             14 | Smith            |          510 | 05-04-2022
+(14 rows)
+
+SELECT * FROM sales_apr2022;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              3 | Ford             |         2000 | 04-30-2022
+(4 rows)
+
+SELECT * FROM sales_apr2022_01_10;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+(2 rows)
+
+SELECT * FROM sales_apr2022_10_20;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              4 | Ivanov           |          750 | 04-13-2022
+(1 row)
+
+SELECT * FROM sales_apr2022_20_30;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              3 | Ford             |         2000 | 04-30-2022
+(1 row)
+
+ALTER TABLE sales_apr2022 MERGE PARTITIONS (sales_apr2022_01_10, sales_apr2022_10_20, sales_apr2022_20_30) INTO sales_apr_all;
+SELECT * FROM sales_range;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              1 | May              |         1000 | 01-31-2022
+             10 | Halder           |          350 | 01-28-2022
+             13 | Gandi            |          377 | 01-09-2022
+              2 | Smirnoff         |          500 | 02-10-2022
+              6 | Poirot           |          150 | 02-11-2022
+              8 | Ericsson         |          185 | 02-23-2022
+              7 | Li               |          175 | 03-08-2022
+              9 | Muller           |          250 | 03-11-2022
+             12 | Plato            |          350 | 03-19-2022
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              3 | Ford             |         2000 | 04-30-2022
+             14 | Smith            |          510 | 05-04-2022
+(14 rows)
+
+SELECT * FROM sales_apr2022;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              3 | Ford             |         2000 | 04-30-2022
+(4 rows)
+
+SELECT * FROM sales_apr_all;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              3 | Ford             |         2000 | 04-30-2022
+(4 rows)
+
+DROP TABLE sales_range;
+--
+-- BY LIST partitioning
+--
+--
+-- Test: specific errors for BY LIST partitioning
+--
+CREATE TABLE sales_list
+(salesperson_id INT GENERATED ALWAYS AS IDENTITY,
+ salesperson_name VARCHAR(30),
+ sales_state VARCHAR(20),
+ sales_amount INT,
+ sales_date DATE)
+PARTITION BY LIST (sales_state);
+CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki');
+CREATE TABLE sales_west PARTITION OF sales_list FOR VALUES IN ('Lisbon', 'New York', 'Madrid');
+CREATE TABLE sales_east PARTITION OF sales_list FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok');
+CREATE TABLE sales_central PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv');
+CREATE TABLE sales_others PARTITION OF sales_list DEFAULT;
+CREATE TABLE sales_list2 (LIKE sales_list) PARTITION BY LIST (sales_state);
+CREATE TABLE sales_nord2 PARTITION OF sales_list2 FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki');
+CREATE TABLE sales_others2 PARTITION OF sales_list2 DEFAULT;
+CREATE TABLE sales_external (LIKE sales_list);
+CREATE TABLE sales_external2 (vch VARCHAR(5));
+-- ERROR:  "sales_external" is not a partition
+ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_external) INTO sales_all;
+ERROR:  "sales_external" is not a partition
+-- ERROR:  "sales_external2" is not a partition
+ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_external2) INTO sales_all;
+ERROR:  "sales_external2" is not a partition
+-- ERROR:  relation "sales_nord2" is not a partition of relation "sales_list"
+ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_nord2, sales_east) INTO sales_all;
+ERROR:  relation "sales_nord2" is not a partition of relation "sales_list"
+DROP TABLE sales_external2;
+DROP TABLE sales_external;
+DROP TABLE sales_list2;
+DROP TABLE sales_list;
+--
+-- Test: BY LIST partitioning, MERGE PARTITIONS with data
+--
+CREATE TABLE sales_list
+(salesperson_id INT GENERATED ALWAYS AS IDENTITY,
+ salesperson_name VARCHAR(30),
+ sales_state VARCHAR(20),
+ sales_amount INT,
+ sales_date DATE)
+PARTITION BY LIST (sales_state);
+CREATE INDEX sales_list_salesperson_name_idx ON sales_list USING btree (salesperson_name);
+CREATE INDEX sales_list_sales_state_idx ON sales_list USING btree (sales_state);
+CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki');
+CREATE TABLE sales_west PARTITION OF sales_list FOR VALUES IN ('Lisbon', 'New York', 'Madrid');
+CREATE TABLE sales_east PARTITION OF sales_list FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok');
+CREATE TABLE sales_central PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv');
+CREATE TABLE sales_others PARTITION OF sales_list DEFAULT;
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Trump',    'Bejing',         1000, '2022-03-01');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Smirnoff', 'New York',        500, '2022-03-03');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Ford',     'St. Petersburg', 2000, '2022-03-05');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Ivanov',   'Warsaw',          750, '2022-03-04');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Deev',     'Lisbon',          250, '2022-03-07');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Poirot',   'Berlin',         1000, '2022-03-01');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Helsinki',       1200, '2022-03-06');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Li',       'Vladivostok',    1150, '2022-03-09');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Helsinki',       1200, '2022-03-11');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Halder',   'Oslo',            800, '2022-03-02');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Muller',   'Madrid',          650, '2022-03-05');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Smith',    'Kyiv',            350, '2022-03-10');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Gandi',    'Warsaw',          150, '2022-03-08');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Plato',    'Lisbon',          950, '2022-03-05');
+-- show partitions with conditions:
+SELECT c.oid::pg_catalog.regclass, c.relkind, inhdetachpending, pg_catalog.pg_get_expr(c.relpartbound, c.oid)
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 'sales_list'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+      oid      | relkind | inhdetachpending |                     pg_get_expr                      
+---------------+---------+------------------+------------------------------------------------------
+ sales_central | r       | f                | FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')
+ sales_east    | r       | f                | FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok')
+ sales_nord    | r       | f                | FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki')
+ sales_west    | r       | f                | FOR VALUES IN ('Lisbon', 'New York', 'Madrid')
+ sales_others  | r       | f                | DEFAULT
+(5 rows)
+
+ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_central) INTO sales_all;
+-- show partitions with conditions:
+SELECT c.oid::pg_catalog.regclass, c.relkind, inhdetachpending, pg_catalog.pg_get_expr(c.relpartbound, c.oid)
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 'sales_list'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+     oid      | relkind | inhdetachpending |                                                 pg_get_expr                                                  
+--------------+---------+------------------+--------------------------------------------------------------------------------------------------------------
+ sales_all    | r       | f                | FOR VALUES IN ('Lisbon', 'New York', 'Madrid', 'Bejing', 'Delhi', 'Vladivostok', 'Warsaw', 'Berlin', 'Kyiv')
+ sales_nord   | r       | f                | FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki')
+ sales_others | r       | f                | DEFAULT
+(3 rows)
+
+SELECT * FROM sales_list;
+ salesperson_id | salesperson_name |  sales_state   | sales_amount | sales_date 
+----------------+------------------+----------------+--------------+------------
+              2 | Smirnoff         | New York       |          500 | 03-03-2022
+              5 | Deev             | Lisbon         |          250 | 03-07-2022
+             11 | Muller           | Madrid         |          650 | 03-05-2022
+             14 | Plato            | Lisbon         |          950 | 03-05-2022
+              1 | Trump            | Bejing         |         1000 | 03-01-2022
+              8 | Li               | Vladivostok    |         1150 | 03-09-2022
+              4 | Ivanov           | Warsaw         |          750 | 03-04-2022
+              6 | Poirot           | Berlin         |         1000 | 03-01-2022
+             12 | Smith            | Kyiv           |          350 | 03-10-2022
+             13 | Gandi            | Warsaw         |          150 | 03-08-2022
+              3 | Ford             | St. Petersburg |         2000 | 03-05-2022
+              7 | May              | Helsinki       |         1200 | 03-06-2022
+              9 | May              | Helsinki       |         1200 | 03-11-2022
+             10 | Halder           | Oslo           |          800 | 03-02-2022
+(14 rows)
+
+SELECT * FROM sales_nord;
+ salesperson_id | salesperson_name |  sales_state   | sales_amount | sales_date 
+----------------+------------------+----------------+--------------+------------
+              3 | Ford             | St. Petersburg |         2000 | 03-05-2022
+              7 | May              | Helsinki       |         1200 | 03-06-2022
+              9 | May              | Helsinki       |         1200 | 03-11-2022
+             10 | Halder           | Oslo           |          800 | 03-02-2022
+(4 rows)
+
+SELECT * FROM sales_all;
+ salesperson_id | salesperson_name | sales_state | sales_amount | sales_date 
+----------------+------------------+-------------+--------------+------------
+              2 | Smirnoff         | New York    |          500 | 03-03-2022
+              5 | Deev             | Lisbon      |          250 | 03-07-2022
+             11 | Muller           | Madrid      |          650 | 03-05-2022
+             14 | Plato            | Lisbon      |          950 | 03-05-2022
+              1 | Trump            | Bejing      |         1000 | 03-01-2022
+              8 | Li               | Vladivostok |         1150 | 03-09-2022
+              4 | Ivanov           | Warsaw      |          750 | 03-04-2022
+              6 | Poirot           | Berlin      |         1000 | 03-01-2022
+             12 | Smith            | Kyiv        |          350 | 03-10-2022
+             13 | Gandi            | Warsaw      |          150 | 03-08-2022
+(10 rows)
+
+-- Use indexscan for testing indexes after merging partitions
+SET enable_seqscan = OFF;
+SELECT * FROM sales_all WHERE sales_state = 'Warsaw';
+ salesperson_id | salesperson_name | sales_state | sales_amount | sales_date 
+----------------+------------------+-------------+--------------+------------
+              4 | Ivanov           | Warsaw      |          750 | 03-04-2022
+             13 | Gandi            | Warsaw      |          150 | 03-08-2022
+(2 rows)
+
+SELECT * FROM sales_list WHERE sales_state = 'Warsaw';
+ salesperson_id | salesperson_name | sales_state | sales_amount | sales_date 
+----------------+------------------+-------------+--------------+------------
+              4 | Ivanov           | Warsaw      |          750 | 03-04-2022
+             13 | Gandi            | Warsaw      |          150 | 03-08-2022
+(2 rows)
+
+SELECT * FROM sales_list WHERE salesperson_name = 'Ivanov';
+ salesperson_id | salesperson_name | sales_state | sales_amount | sales_date 
+----------------+------------------+-------------+--------------+------------
+              4 | Ivanov           | Warsaw      |          750 | 03-04-2022
+(1 row)
+
+RESET enable_seqscan;
+DROP TABLE sales_list;
+--
+-- Try to MERGE partitions of another table.
+--
+CREATE TABLE t1 (i int, a int, b int, c int) PARTITION BY RANGE (a, b);
+CREATE TABLE t1p1 PARTITION OF t1 FOR VALUES FROM (1, 1) TO (1, 2);
+CREATE TABLE t2 (i int, t text) PARTITION BY RANGE (t);
+CREATE TABLE t2pa PARTITION OF t2 FOR VALUES FROM ('A') TO ('C');
+CREATE TABLE t3 (i int, t text);
+-- ERROR:  relation "t1p1" is not a partition of relation "t2"
+ALTER TABLE t2 MERGE PARTITIONS (t1p1, t2pa) INTO t2p;
+ERROR:  relation "t1p1" is not a partition of relation "t2"
+-- ERROR:  "t3" is not a partition
+ALTER TABLE t2 MERGE PARTITIONS (t2pa, t3) INTO t2p;
+ERROR:  "t3" is not a partition
+DROP TABLE t3;
+DROP TABLE t2;
+DROP TABLE t1;
+--
+-- Try to MERGE partitions of temporary table.
+--
+CREATE TEMP TABLE t (i int) PARTITION BY RANGE (i);
+CREATE TEMP TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1);
+CREATE TEMP TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2);
+SELECT c.oid::pg_catalog.regclass, pg_catalog.pg_get_expr(c.relpartbound, c.oid), c.relpersistence
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 't'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+  oid   |        pg_get_expr         | relpersistence 
+--------+----------------------------+----------------
+ tp_0_1 | FOR VALUES FROM (0) TO (1) | t
+ tp_1_2 | FOR VALUES FROM (1) TO (2) | t
+(2 rows)
+
+ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2;
+-- Partition should be temporary.
+SELECT c.oid::pg_catalog.regclass, pg_catalog.pg_get_expr(c.relpartbound, c.oid), c.relpersistence
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 't'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+  oid   |        pg_get_expr         | relpersistence 
+--------+----------------------------+----------------
+ tp_0_2 | FOR VALUES FROM (0) TO (2) | t
+(1 row)
+
+DROP TABLE t;
+--
+-- Check the partition index name if the partition name is the same as one
+-- of the merged partitions.
+--
+CREATE TABLE t (i int, PRIMARY KEY(i)) 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 INDEX tidx ON t(i);
+ALTER TABLE t MERGE PARTITIONS (tp_1_2, tp_0_1) INTO tp_1_2;
+-- Indexname values should be 'tp_1_2_pkey' and 'tp_1_2_i_idx'.
+-- Not-null constraint name should be 'tp_1_2_i_not_null'.
+\d+ tp_1_2
+                          Table "partitions_merge_schema.tp_1_2"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ i      | integer |           | not null |         | plain   |              | 
+Partition of: t FOR VALUES FROM (0) TO (2)
+Partition constraint: ((i IS NOT NULL) AND (i >= 0) AND (i < 2))
+Indexes:
+    "tp_1_2_pkey" PRIMARY KEY, btree (i)
+    "tp_1_2_i_idx" btree (i)
+
+DROP TABLE t;
+--
+-- Try mixing permanent and temporary partitions.
+--
+SET search_path = partitions_merge_schema, pg_temp, public;
+CREATE TABLE t (i int) 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);
+SELECT c.oid::pg_catalog.regclass, c.relpersistence FROM pg_catalog.pg_class c WHERE c.oid = 't'::regclass;
+ oid | relpersistence 
+-----+----------------
+ t   | p
+(1 row)
+
+SELECT c.oid::pg_catalog.regclass, pg_catalog.pg_get_expr(c.relpartbound, c.oid), c.relpersistence
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 't'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+  oid   |        pg_get_expr         | relpersistence 
+--------+----------------------------+----------------
+ tp_0_1 | FOR VALUES FROM (0) TO (1) | p
+ tp_1_2 | FOR VALUES FROM (1) TO (2) | p
+(2 rows)
+
+SET search_path = pg_temp, partitions_merge_schema, public;
+-- Can't merge persistent partitions into a temporary partition
+ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2;
+ERROR:  cannot create a temporary relation as partition of permanent relation "t"
+SET search_path = partitions_merge_schema, public;
+-- Can't merge persistent partitions into a temporary partition
+ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO pg_temp.tp_0_2;
+ERROR:  cannot create a temporary relation as partition of permanent relation "t"
+DROP TABLE t;
+SET search_path = pg_temp, partitions_merge_schema, public;
+BEGIN;
+CREATE TABLE t (i int) 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);
+SELECT c.oid::pg_catalog.regclass, c.relpersistence FROM pg_catalog.pg_class c WHERE c.oid = 't'::regclass;
+ oid | relpersistence 
+-----+----------------
+ t   | t
+(1 row)
+
+SELECT c.oid::pg_catalog.regclass, pg_catalog.pg_get_expr(c.relpartbound, c.oid), c.relpersistence
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 't'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+  oid   |        pg_get_expr         | relpersistence 
+--------+----------------------------+----------------
+ tp_0_1 | FOR VALUES FROM (0) TO (1) | t
+ tp_1_2 | FOR VALUES FROM (1) TO (2) | t
+(2 rows)
+
+SET search_path = partitions_merge_schema, pg_temp, public;
+-- Can't merge temporary partitions into a persistent partition
+ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2;
+ROLLBACK;
+-- Check the new partition inherits parent's tablespace
+SET search_path = partitions_merge_schema, public;
+CREATE TABLE t (i int PRIMARY KEY USING INDEX TABLESPACE regress_tblspace)
+  PARTITION BY RANGE (i) TABLESPACE regress_tblspace;
+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 t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2;
+SELECT tablename, tablespace FROM pg_tables
+  WHERE tablename IN ('t', 'tp_0_2') AND schemaname = 'partitions_merge_schema'
+  ORDER BY tablename, tablespace;
+ tablename |    tablespace    
+-----------+------------------
+ t         | regress_tblspace
+ tp_0_2    | regress_tblspace
+(2 rows)
+
+SELECT tablename, indexname, tablespace FROM pg_indexes
+  WHERE tablename IN ('t', 'tp_0_2') AND schemaname = 'partitions_merge_schema'
+  ORDER BY tablename, indexname, tablespace;
+ tablename |  indexname  |    tablespace    
+-----------+-------------+------------------
+ t         | t_pkey      | regress_tblspace
+ tp_0_2    | tp_0_2_pkey | regress_tblspace
+(2 rows)
+
+DROP TABLE t;
+-- Check the new partition inherits parent's table access method
+SET search_path = partitions_merge_schema, public;
+CREATE ACCESS METHOD partitions_merge_heap TYPE TABLE HANDLER heap_tableam_handler;
+CREATE TABLE t (i int) PARTITION BY RANGE (i) USING partitions_merge_heap;
+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 t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2;
+SELECT c.relname, a.amname
+FROM pg_class c JOIN pg_am a ON c.relam = a.oid
+WHERE c.oid IN ('t'::regclass, 'tp_0_2'::regclass)
+ORDER BY c.relname;
+ relname |        amname         
+---------+-----------------------
+ t       | partitions_merge_heap
+ tp_0_2  | partitions_merge_heap
+(2 rows)
+
+DROP TABLE t;
+DROP ACCESS METHOD partitions_merge_heap;
+-- Test permission checks.  The user needs to own the parent table and all
+-- the merging partitions to do the merge.
+CREATE ROLE regress_partition_merge_alice;
+CREATE ROLE regress_partition_merge_bob;
+GRANT ALL ON SCHEMA partitions_merge_schema TO regress_partition_merge_alice;
+GRANT ALL ON SCHEMA partitions_merge_schema TO regress_partition_merge_bob;
+SET SESSION AUTHORIZATION regress_partition_merge_alice;
+CREATE TABLE t (i int) 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);
+SET SESSION AUTHORIZATION regress_partition_merge_bob;
+ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2;
+ERROR:  must be owner of table t
+RESET SESSION AUTHORIZATION;
+ALTER TABLE t OWNER TO regress_partition_merge_bob;
+SET SESSION AUTHORIZATION regress_partition_merge_bob;
+ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2;
+ERROR:  must be owner of table tp_0_1
+RESET SESSION AUTHORIZATION;
+ALTER TABLE tp_0_1 OWNER TO regress_partition_merge_bob;
+SET SESSION AUTHORIZATION regress_partition_merge_bob;
+ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2;
+ERROR:  must be owner of table tp_1_2
+RESET SESSION AUTHORIZATION;
+ALTER TABLE tp_1_2 OWNER TO regress_partition_merge_bob;
+SET SESSION AUTHORIZATION regress_partition_merge_bob;
+ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2;
+RESET SESSION AUTHORIZATION;
+DROP TABLE t;
+REVOKE ALL ON SCHEMA partitions_merge_schema FROM regress_partition_merge_alice;
+REVOKE ALL ON SCHEMA partitions_merge_schema FROM regress_partition_merge_bob;
+DROP ROLE regress_partition_merge_alice;
+DROP ROLE regress_partition_merge_bob;
+RESET search_path;
+--
+DROP SCHEMA partitions_merge_schema;
+DROP SCHEMA partitions_merge_schema2;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f53a526f7c..0014801684 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -119,7 +119,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # The stats test resets stats, so nothing else needing stats access can be in
 # this group.
 # ----------
-test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate
+test: partition_merge partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate
 
 # event_trigger depends on create_am and cannot run concurrently with
 # any test that runs DDL
diff --git a/src/test/regress/sql/partition_merge.sql b/src/test/regress/sql/partition_merge.sql
new file mode 100644
index 0000000000..bede819af9
--- /dev/null
+++ b/src/test/regress/sql/partition_merge.sql
@@ -0,0 +1,609 @@
+--
+-- PARTITIONS_MERGE
+-- Tests for "ALTER TABLE ... MERGE PARTITIONS ..." command
+--
+
+CREATE SCHEMA partitions_merge_schema;
+CREATE SCHEMA partitions_merge_schema2;
+SET search_path = partitions_merge_schema, public;
+
+--
+-- BY RANGE partitioning
+--
+
+--
+-- Test for error codes
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_dec2021 PARTITION OF sales_range FOR VALUES FROM ('2021-12-01') TO ('2021-12-31');
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-03-01');
+CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM ('2022-03-01') TO ('2022-04-01');
+
+CREATE TABLE sales_apr2022 (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_apr_1 PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-04-15');
+CREATE TABLE sales_apr_2 PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-15') TO ('2022-05-01');
+ALTER TABLE sales_range ATTACH PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01');
+
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+
+-- ERROR:  partition with name "sales_feb2022" is already used
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_feb2022) INTO sales_feb_mar_apr2022;
+-- ERROR:  "sales_apr2022" is not a table
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_apr2022) INTO sales_feb_mar_apr2022;
+-- ERROR:  lower bound of partition "sales_mar2022" conflicts with upper bound of previous partition "sales_jan2022"
+-- (space between sections sales_jan2022 and sales_mar2022)
+ALTER TABLE sales_range MERGE PARTITIONS (sales_jan2022, sales_mar2022) INTO sales_jan_mar2022;
+-- ERROR:  lower bound of partition "sales_jan2022" conflicts with upper bound of previous partition "sales_dec2021"
+-- (space between sections sales_dec2021 and sales_jan2022)
+ALTER TABLE sales_range MERGE PARTITIONS (sales_dec2021, sales_jan2022, sales_feb2022) INTO sales_dec_jan_feb2022;
+
+-- NO ERROR: test for custom partitions order, source partitions not in the search_path
+SET search_path = partitions_merge_schema2, public;
+ALTER TABLE partitions_merge_schema.sales_range MERGE PARTITIONS (
+  partitions_merge_schema.sales_feb2022,
+  partitions_merge_schema.sales_mar2022,
+  partitions_merge_schema.sales_jan2022) INTO sales_jan_feb_mar2022;
+SET search_path = partitions_merge_schema, public;
+
+SELECT c.oid::pg_catalog.regclass, c.relkind, inhdetachpending, pg_catalog.pg_get_expr(c.relpartbound, c.oid)
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 'sales_range'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+
+DROP TABLE sales_range;
+
+--
+-- Add rows into partitioned table, then merge partitions
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-03-01');
+CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM ('2022-03-01') TO ('2022-04-01');
+CREATE TABLE sales_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-04-01') TO ('2022-05-01');
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree (sales_date);
+
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-10');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
+INSERT INTO sales_range VALUES (4,  'Ivanov',   750,  '2022-04-13');
+INSERT INTO sales_range VALUES (5,  'Deev',     250,  '2022-04-07');
+INSERT INTO sales_range VALUES (6,  'Poirot',   150,  '2022-02-11');
+INSERT INTO sales_range VALUES (7,  'Li',       175,  '2022-03-08');
+INSERT INTO sales_range VALUES (8,  'Ericsson', 185,  '2022-02-23');
+INSERT INTO sales_range VALUES (9,  'Muller',   250,  '2022-03-11');
+INSERT INTO sales_range VALUES (10, 'Halder',   350,  '2022-01-28');
+INSERT INTO sales_range VALUES (11, 'Trump',    380,  '2022-04-06');
+INSERT INTO sales_range VALUES (12, 'Plato',    350,  '2022-03-19');
+INSERT INTO sales_range VALUES (13, 'Gandi',    377,  '2022-01-09');
+INSERT INTO sales_range VALUES (14, 'Smith',    510,  '2022-05-04');
+
+SELECT pg_catalog.pg_get_partkeydef('sales_range'::regclass);
+
+-- show partitions with conditions:
+SELECT c.oid::pg_catalog.regclass, c.relkind, inhdetachpending, pg_catalog.pg_get_expr(c.relpartbound, c.oid)
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 'sales_range'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+
+-- check schema-qualified name of the new partition
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_apr2022) INTO partitions_merge_schema2.sales_feb_mar_apr2022;
+
+-- show partitions with conditions:
+SELECT c.oid::pg_catalog.regclass, c.relkind, inhdetachpending, pg_catalog.pg_get_expr(c.relpartbound, c.oid)
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 'sales_range'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+
+SELECT * FROM pg_indexes WHERE tablename = 'sales_feb_mar_apr2022' and schemaname = 'partitions_merge_schema2';
+
+SELECT * FROM sales_range;
+SELECT * FROM sales_jan2022;
+SELECT * FROM partitions_merge_schema2.sales_feb_mar_apr2022;
+SELECT * FROM sales_others;
+
+-- Use indexscan for testing indexes
+SET enable_seqscan = OFF;
+
+SELECT * FROM partitions_merge_schema2.sales_feb_mar_apr2022 where sales_date > '2022-01-01';
+
+RESET enable_seqscan;
+
+DROP TABLE sales_range;
+
+--
+-- Merge some partitions into DEFAULT partition
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-03-01');
+CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM ('2022-03-01') TO ('2022-04-01');
+CREATE TABLE sales_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-04-01') TO ('2022-05-01');
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree (sales_date);
+
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-10');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
+INSERT INTO sales_range VALUES (4,  'Ivanov',   750,  '2022-04-13');
+INSERT INTO sales_range VALUES (5,  'Deev',     250,  '2022-04-07');
+INSERT INTO sales_range VALUES (6,  'Poirot',   150,  '2022-02-11');
+INSERT INTO sales_range VALUES (7,  'Li',       175,  '2022-03-08');
+INSERT INTO sales_range VALUES (8,  'Ericsson', 185,  '2022-02-23');
+INSERT INTO sales_range VALUES (9,  'Muller',   250,  '2022-03-11');
+INSERT INTO sales_range VALUES (10, 'Halder',   350,  '2022-01-28');
+INSERT INTO sales_range VALUES (11, 'Trump',    380,  '2022-04-06');
+INSERT INTO sales_range VALUES (12, 'Plato',    350,  '2022-03-19');
+INSERT INTO sales_range VALUES (13, 'Gandi',    377,  '2022-01-09');
+INSERT INTO sales_range VALUES (14, 'Smith',    510,  '2022-05-04');
+
+-- Merge partitions (include DEFAULT partition) into partition with the same
+-- name
+ALTER TABLE sales_range MERGE PARTITIONS
+  (sales_jan2022, sales_mar2022, partitions_merge_schema.sales_others) INTO sales_others;
+
+select * from sales_others;
+
+-- show partitions with conditions:
+SELECT c.oid::pg_catalog.regclass, c.relkind, inhdetachpending, pg_catalog.pg_get_expr(c.relpartbound, c.oid)
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 'sales_range'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+
+DROP TABLE sales_range;
+
+--
+-- Test for:
+--   * composite partition key;
+--   * GENERATED column;
+--   * column with DEFAULT value.
+--
+CREATE TABLE sales_date (salesperson_name VARCHAR(30), sales_year INT, sales_month INT, sales_day INT,
+  sales_date VARCHAR(10) GENERATED ALWAYS AS
+    (LPAD(sales_year::text, 4, '0') || '.' || LPAD(sales_month::text, 2, '0') || '.' || LPAD(sales_day::text, 2, '0')) STORED,
+  sales_department VARCHAR(30) DEFAULT 'Sales department')
+  PARTITION BY RANGE (sales_year, sales_month, sales_day);
+
+CREATE TABLE sales_dec2022 PARTITION OF sales_date FOR VALUES FROM (2021, 12, 1) TO (2022, 1, 1);
+CREATE TABLE sales_jan2022 PARTITION OF sales_date FOR VALUES FROM (2022, 1, 1) TO (2022, 2, 1);
+CREATE TABLE sales_feb2022 PARTITION OF sales_date FOR VALUES FROM (2022, 2, 1) TO (2022, 3, 1);
+CREATE TABLE sales_other PARTITION OF sales_date FOR VALUES FROM (2022, 3, 1) TO (MAXVALUE, MAXVALUE, MAXVALUE);
+
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2021, 12, 7);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2021, 12, 8);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 1, 1);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 2, 4);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 1, 2);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 2, 1);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 3, 3);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 3, 4);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 5, 1);
+
+SELECT * FROM sales_date;
+SELECT * FROM sales_dec2022;
+SELECT * FROM sales_jan2022;
+SELECT * FROM sales_feb2022;
+SELECT * FROM sales_other;
+
+ALTER TABLE sales_date MERGE PARTITIONS (sales_jan2022, sales_feb2022) INTO sales_jan_feb2022;
+
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 1, 10);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 2, 10);
+
+SELECT * FROM sales_date;
+SELECT * FROM sales_dec2022;
+SELECT * FROM sales_jan_feb2022;
+SELECT * FROM sales_other;
+
+DROP TABLE sales_date;
+
+--
+-- Test: merge partitions of partitioned table with triggers
+--
+CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id);
+
+CREATE TABLE salespeople01_10 PARTITION OF salespeople FOR VALUES FROM (1) TO (10);
+CREATE TABLE salespeople10_20 PARTITION OF salespeople FOR VALUES FROM (10) TO (20);
+CREATE TABLE salespeople20_30 PARTITION OF salespeople FOR VALUES FROM (20) TO (30);
+CREATE TABLE salespeople30_40 PARTITION OF salespeople FOR VALUES FROM (30) TO (40);
+
+INSERT INTO salespeople VALUES (1,  'Poirot');
+
+CREATE OR REPLACE FUNCTION after_insert_row_trigger() RETURNS trigger LANGUAGE 'plpgsql' AS $BODY$
+BEGIN
+	RAISE NOTICE 'trigger(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
+    RETURN NULL;
+END;
+$BODY$;
+
+CREATE TRIGGER salespeople_after_insert_statement_trigger
+    AFTER INSERT
+    ON salespeople
+    FOR EACH STATEMENT
+    EXECUTE PROCEDURE after_insert_row_trigger('salespeople');
+
+CREATE TRIGGER salespeople_after_insert_row_trigger
+    AFTER INSERT
+    ON salespeople
+    FOR EACH ROW
+    EXECUTE PROCEDURE after_insert_row_trigger('salespeople');
+
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salespeople VALUES (10, 'May');
+-- 1 trigger should fire here (row):
+INSERT INTO salespeople10_20 VALUES (19, 'Ivanov');
+
+ALTER TABLE salespeople MERGE PARTITIONS (salespeople10_20, salespeople20_30, salespeople30_40) INTO salespeople10_40;
+
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salespeople VALUES (20, 'Smirnoff');
+-- 1 trigger should fire here (row):
+INSERT INTO salespeople10_40 VALUES (30, 'Ford');
+
+SELECT * FROM salespeople01_10;
+SELECT * FROM salespeople10_40;
+
+DROP TABLE salespeople;
+DROP FUNCTION after_insert_row_trigger();
+
+--
+-- Test: merge partitions with deleted columns
+--
+CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id);
+
+CREATE TABLE salespeople01_10 PARTITION OF salespeople FOR VALUES FROM (1) TO (10);
+-- Create partitions with some deleted columns:
+CREATE TABLE salespeople10_20(d1 VARCHAR(30), salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30));
+CREATE TABLE salespeople20_30(salesperson_id INT PRIMARY KEY, d2 INT,  salesperson_name VARCHAR(30));
+CREATE TABLE salespeople30_40(salesperson_id INT PRIMARY KEY, d3 DATE, salesperson_name VARCHAR(30));
+
+INSERT INTO salespeople10_20 VALUES ('dummy value 1', 19, 'Ivanov');
+INSERT INTO salespeople20_30 VALUES (20, 101, 'Smirnoff');
+INSERT INTO salespeople30_40 VALUES (31, now(), 'Popov');
+
+ALTER TABLE salespeople10_20 DROP COLUMN d1;
+ALTER TABLE salespeople20_30 DROP COLUMN d2;
+ALTER TABLE salespeople30_40 DROP COLUMN d3;
+
+ALTER TABLE salespeople ATTACH PARTITION salespeople10_20 FOR VALUES FROM (10) TO (20);
+ALTER TABLE salespeople ATTACH PARTITION salespeople20_30 FOR VALUES FROM (20) TO (30);
+ALTER TABLE salespeople ATTACH PARTITION salespeople30_40 FOR VALUES FROM (30) TO (40);
+
+INSERT INTO salespeople VALUES (1, 'Poirot');
+INSERT INTO salespeople VALUES (10, 'May');
+INSERT INTO salespeople VALUES (30, 'Ford');
+
+ALTER TABLE salespeople MERGE PARTITIONS (salespeople10_20, salespeople20_30, salespeople30_40) INTO salespeople10_40;
+
+select * from salespeople;
+select * from salespeople01_10;
+select * from salespeople10_40;
+
+DROP TABLE salespeople;
+
+--
+-- Test: merge sub-partitions
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-03-01');
+CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM ('2022-03-01') TO ('2022-04-01');
+
+CREATE TABLE sales_apr2022 (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_apr2022_01_10 PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-04-10');
+CREATE TABLE sales_apr2022_10_20 PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-10') TO ('2022-04-20');
+CREATE TABLE sales_apr2022_20_30 PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-20') TO ('2022-05-01');
+ALTER TABLE sales_range ATTACH PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01');
+
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+
+CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree (sales_date);
+
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-10');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
+INSERT INTO sales_range VALUES (4,  'Ivanov',   750,  '2022-04-13');
+INSERT INTO sales_range VALUES (5,  'Deev',     250,  '2022-04-07');
+INSERT INTO sales_range VALUES (6,  'Poirot',   150,  '2022-02-11');
+INSERT INTO sales_range VALUES (7,  'Li',       175,  '2022-03-08');
+INSERT INTO sales_range VALUES (8,  'Ericsson', 185,  '2022-02-23');
+INSERT INTO sales_range VALUES (9,  'Muller',   250,  '2022-03-11');
+INSERT INTO sales_range VALUES (10, 'Halder',   350,  '2022-01-28');
+INSERT INTO sales_range VALUES (11, 'Trump',    380,  '2022-04-06');
+INSERT INTO sales_range VALUES (12, 'Plato',    350,  '2022-03-19');
+INSERT INTO sales_range VALUES (13, 'Gandi',    377,  '2022-01-09');
+INSERT INTO sales_range VALUES (14, 'Smith',    510,  '2022-05-04');
+
+SELECT * FROM sales_range;
+SELECT * FROM sales_apr2022;
+SELECT * FROM sales_apr2022_01_10;
+SELECT * FROM sales_apr2022_10_20;
+SELECT * FROM sales_apr2022_20_30;
+
+ALTER TABLE sales_apr2022 MERGE PARTITIONS (sales_apr2022_01_10, sales_apr2022_10_20, sales_apr2022_20_30) INTO sales_apr_all;
+
+SELECT * FROM sales_range;
+SELECT * FROM sales_apr2022;
+SELECT * FROM sales_apr_all;
+
+DROP TABLE sales_range;
+
+--
+-- BY LIST partitioning
+--
+
+--
+-- Test: specific errors for BY LIST partitioning
+--
+CREATE TABLE sales_list
+(salesperson_id INT GENERATED ALWAYS AS IDENTITY,
+ salesperson_name VARCHAR(30),
+ sales_state VARCHAR(20),
+ sales_amount INT,
+ sales_date DATE)
+PARTITION BY LIST (sales_state);
+CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki');
+CREATE TABLE sales_west PARTITION OF sales_list FOR VALUES IN ('Lisbon', 'New York', 'Madrid');
+CREATE TABLE sales_east PARTITION OF sales_list FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok');
+CREATE TABLE sales_central PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv');
+CREATE TABLE sales_others PARTITION OF sales_list DEFAULT;
+
+
+CREATE TABLE sales_list2 (LIKE sales_list) PARTITION BY LIST (sales_state);
+CREATE TABLE sales_nord2 PARTITION OF sales_list2 FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki');
+CREATE TABLE sales_others2 PARTITION OF sales_list2 DEFAULT;
+
+
+CREATE TABLE sales_external (LIKE sales_list);
+CREATE TABLE sales_external2 (vch VARCHAR(5));
+
+-- ERROR:  "sales_external" is not a partition
+ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_external) INTO sales_all;
+-- ERROR:  "sales_external2" is not a partition
+ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_external2) INTO sales_all;
+-- ERROR:  relation "sales_nord2" is not a partition of relation "sales_list"
+ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_nord2, sales_east) INTO sales_all;
+
+DROP TABLE sales_external2;
+DROP TABLE sales_external;
+DROP TABLE sales_list2;
+DROP TABLE sales_list;
+
+--
+-- Test: BY LIST partitioning, MERGE PARTITIONS with data
+--
+CREATE TABLE sales_list
+(salesperson_id INT GENERATED ALWAYS AS IDENTITY,
+ salesperson_name VARCHAR(30),
+ sales_state VARCHAR(20),
+ sales_amount INT,
+ sales_date DATE)
+PARTITION BY LIST (sales_state);
+
+CREATE INDEX sales_list_salesperson_name_idx ON sales_list USING btree (salesperson_name);
+CREATE INDEX sales_list_sales_state_idx ON sales_list USING btree (sales_state);
+
+CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki');
+CREATE TABLE sales_west PARTITION OF sales_list FOR VALUES IN ('Lisbon', 'New York', 'Madrid');
+CREATE TABLE sales_east PARTITION OF sales_list FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok');
+CREATE TABLE sales_central PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv');
+CREATE TABLE sales_others PARTITION OF sales_list DEFAULT;
+
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Trump',    'Bejing',         1000, '2022-03-01');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Smirnoff', 'New York',        500, '2022-03-03');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Ford',     'St. Petersburg', 2000, '2022-03-05');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Ivanov',   'Warsaw',          750, '2022-03-04');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Deev',     'Lisbon',          250, '2022-03-07');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Poirot',   'Berlin',         1000, '2022-03-01');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Helsinki',       1200, '2022-03-06');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Li',       'Vladivostok',    1150, '2022-03-09');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Helsinki',       1200, '2022-03-11');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Halder',   'Oslo',            800, '2022-03-02');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Muller',   'Madrid',          650, '2022-03-05');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Smith',    'Kyiv',            350, '2022-03-10');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Gandi',    'Warsaw',          150, '2022-03-08');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Plato',    'Lisbon',          950, '2022-03-05');
+
+-- show partitions with conditions:
+SELECT c.oid::pg_catalog.regclass, c.relkind, inhdetachpending, pg_catalog.pg_get_expr(c.relpartbound, c.oid)
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 'sales_list'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+
+ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_central) INTO sales_all;
+
+-- show partitions with conditions:
+SELECT c.oid::pg_catalog.regclass, c.relkind, inhdetachpending, pg_catalog.pg_get_expr(c.relpartbound, c.oid)
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 'sales_list'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+
+SELECT * FROM sales_list;
+SELECT * FROM sales_nord;
+SELECT * FROM sales_all;
+
+-- Use indexscan for testing indexes after merging partitions
+SET enable_seqscan = OFF;
+
+SELECT * FROM sales_all WHERE sales_state = 'Warsaw';
+SELECT * FROM sales_list WHERE sales_state = 'Warsaw';
+SELECT * FROM sales_list WHERE salesperson_name = 'Ivanov';
+
+RESET enable_seqscan;
+
+DROP TABLE sales_list;
+
+--
+-- Try to MERGE partitions of another table.
+--
+CREATE TABLE t1 (i int, a int, b int, c int) PARTITION BY RANGE (a, b);
+CREATE TABLE t1p1 PARTITION OF t1 FOR VALUES FROM (1, 1) TO (1, 2);
+CREATE TABLE t2 (i int, t text) PARTITION BY RANGE (t);
+CREATE TABLE t2pa PARTITION OF t2 FOR VALUES FROM ('A') TO ('C');
+CREATE TABLE t3 (i int, t text);
+
+-- ERROR:  relation "t1p1" is not a partition of relation "t2"
+ALTER TABLE t2 MERGE PARTITIONS (t1p1, t2pa) INTO t2p;
+-- ERROR:  "t3" is not a partition
+ALTER TABLE t2 MERGE PARTITIONS (t2pa, t3) INTO t2p;
+
+DROP TABLE t3;
+DROP TABLE t2;
+DROP TABLE t1;
+
+--
+-- Try to MERGE partitions of temporary table.
+--
+CREATE TEMP TABLE t (i int) PARTITION BY RANGE (i);
+CREATE TEMP TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1);
+CREATE TEMP TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2);
+
+SELECT c.oid::pg_catalog.regclass, pg_catalog.pg_get_expr(c.relpartbound, c.oid), c.relpersistence
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 't'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+
+ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2;
+
+-- Partition should be temporary.
+SELECT c.oid::pg_catalog.regclass, pg_catalog.pg_get_expr(c.relpartbound, c.oid), c.relpersistence
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 't'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+
+DROP TABLE t;
+
+--
+-- Check the partition index name if the partition name is the same as one
+-- of the merged partitions.
+--
+CREATE TABLE t (i int, PRIMARY KEY(i)) 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 INDEX tidx ON t(i);
+ALTER TABLE t MERGE PARTITIONS (tp_1_2, tp_0_1) INTO tp_1_2;
+
+-- Indexname values should be 'tp_1_2_pkey' and 'tp_1_2_i_idx'.
+-- Not-null constraint name should be 'tp_1_2_i_not_null'.
+\d+ tp_1_2
+
+DROP TABLE t;
+
+--
+-- Try mixing permanent and temporary partitions.
+--
+SET search_path = partitions_merge_schema, pg_temp, public;
+CREATE TABLE t (i int) 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);
+
+SELECT c.oid::pg_catalog.regclass, c.relpersistence FROM pg_catalog.pg_class c WHERE c.oid = 't'::regclass;
+SELECT c.oid::pg_catalog.regclass, pg_catalog.pg_get_expr(c.relpartbound, c.oid), c.relpersistence
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 't'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+
+SET search_path = pg_temp, partitions_merge_schema, public;
+
+-- Can't merge persistent partitions into a temporary partition
+ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2;
+
+SET search_path = partitions_merge_schema, public;
+
+-- Can't merge persistent partitions into a temporary partition
+ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO pg_temp.tp_0_2;
+DROP TABLE t;
+
+SET search_path = pg_temp, partitions_merge_schema, public;
+
+BEGIN;
+CREATE TABLE t (i int) 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);
+
+SELECT c.oid::pg_catalog.regclass, c.relpersistence FROM pg_catalog.pg_class c WHERE c.oid = 't'::regclass;
+SELECT c.oid::pg_catalog.regclass, pg_catalog.pg_get_expr(c.relpartbound, c.oid), c.relpersistence
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 't'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+
+SET search_path = partitions_merge_schema, pg_temp, public;
+
+-- Can't merge temporary partitions into a persistent partition
+ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2;
+ROLLBACK;
+
+-- Check the new partition inherits parent's tablespace
+SET search_path = partitions_merge_schema, public;
+CREATE TABLE t (i int PRIMARY KEY USING INDEX TABLESPACE regress_tblspace)
+  PARTITION BY RANGE (i) TABLESPACE regress_tblspace;
+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 t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2;
+SELECT tablename, tablespace FROM pg_tables
+  WHERE tablename IN ('t', 'tp_0_2') AND schemaname = 'partitions_merge_schema'
+  ORDER BY tablename, tablespace;
+SELECT tablename, indexname, tablespace FROM pg_indexes
+  WHERE tablename IN ('t', 'tp_0_2') AND schemaname = 'partitions_merge_schema'
+  ORDER BY tablename, indexname, tablespace;
+DROP TABLE t;
+
+-- Check the new partition inherits parent's table access method
+SET search_path = partitions_merge_schema, public;
+CREATE ACCESS METHOD partitions_merge_heap TYPE TABLE HANDLER heap_tableam_handler;
+CREATE TABLE t (i int) PARTITION BY RANGE (i) USING partitions_merge_heap;
+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 t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2;
+SELECT c.relname, a.amname
+FROM pg_class c JOIN pg_am a ON c.relam = a.oid
+WHERE c.oid IN ('t'::regclass, 'tp_0_2'::regclass)
+ORDER BY c.relname;
+DROP TABLE t;
+DROP ACCESS METHOD partitions_merge_heap;
+
+-- Test permission checks.  The user needs to own the parent table and all
+-- the merging partitions to do the merge.
+CREATE ROLE regress_partition_merge_alice;
+CREATE ROLE regress_partition_merge_bob;
+GRANT ALL ON SCHEMA partitions_merge_schema TO regress_partition_merge_alice;
+GRANT ALL ON SCHEMA partitions_merge_schema TO regress_partition_merge_bob;
+
+SET SESSION AUTHORIZATION regress_partition_merge_alice;
+CREATE TABLE t (i int) 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);
+
+SET SESSION AUTHORIZATION regress_partition_merge_bob;
+ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2;
+RESET SESSION AUTHORIZATION;
+
+ALTER TABLE t OWNER TO regress_partition_merge_bob;
+SET SESSION AUTHORIZATION regress_partition_merge_bob;
+ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2;
+RESET SESSION AUTHORIZATION;
+
+ALTER TABLE tp_0_1 OWNER TO regress_partition_merge_bob;
+SET SESSION AUTHORIZATION regress_partition_merge_bob;
+ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2;
+RESET SESSION AUTHORIZATION;
+
+ALTER TABLE tp_1_2 OWNER TO regress_partition_merge_bob;
+SET SESSION AUTHORIZATION regress_partition_merge_bob;
+ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2;
+RESET SESSION AUTHORIZATION;
+
+DROP TABLE t;
+REVOKE ALL ON SCHEMA partitions_merge_schema FROM regress_partition_merge_alice;
+REVOKE ALL ON SCHEMA partitions_merge_schema FROM regress_partition_merge_bob;
+DROP ROLE regress_partition_merge_alice;
+DROP ROLE regress_partition_merge_bob;
+
+RESET search_path;
+
+--
+DROP SCHEMA partitions_merge_schema;
+DROP SCHEMA partitions_merge_schema2;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9e951a9e6f..ddc4c78fb9 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2659,6 +2659,7 @@ SimpleStats
 SimpleStringList
 SimpleStringListCell
 SingleBoundSortItem
+SinglePartitionSpec
 Size
 SkipPages
 SlabBlock
-- 
2.40.1.windows.1

From 71a05398ce52c634cdf3a1cee45ada938118d125 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:58:09 +0300
Subject: [PATCH v32 2/2] Implement ALTER TABLE ... SPLIT PARTITION ... command

This new DDL command splits a single partition into several parititions.
Just like ALTER TABLE ... MERGE PARTITIONS ... command, new patitions are
created using createPartitionTable() function with parent partition as the
template.

This commit comprises quite naive implementation which works in single process
and holds the ACCESS EXCLUSIVE LOCK on the parent table during all the
operations including the tuple routing.  This is why this new DDL command
can't be recommended for large partitioned tables under a high load.  However,
this implementation come in handy in certain cases even as is.
Also, it could be used as a foundation for future implementations with lesser
locking and possibly parallel.

Discussion: https://postgr.es/m/c73a1746-0cd0-6bdd-6b23-3ae0b7c0c582%40postgrespro.ru
Author: Dmitry Koval
Reviewed-by: Matthias van de Meent, Laurenz Albe, Zhihong Yu, Justin Pryzby
Reviewed-by: Alvaro Herrera, Robert Haas, Stephane Tachoires

Fixes (summary information).
Authors: Alexander Korotkov, Tender Wang, Richard Guo, Dagfinn Ilmari Mannsaker
Authors: Fujii Masao
Reviewed-by: Alexander Korotkov, Robert Haas, Justin Pryzby, Pavel Borisov
Reviewed-by: Masahiko Sawada
Reported-by: Alexander Lakhin, Justin Pryzby, Kyotaro Horiguchi
Reported-by: Daniel Gustafsson, Tom Lane, Noah Misch
---
 doc/src/sgml/ddl.sgml                         |   19 +
 doc/src/sgml/ref/alter_table.sgml             |   79 +-
 src/backend/commands/tablecmds.c              |  398 +++++
 src/backend/parser/gram.y                     |   38 +-
 src/backend/parser/parse_utilcmd.c            |   64 +-
 src/backend/partitioning/partbounds.c         |  689 +++++++
 src/backend/utils/adt/ruleutils.c             |   18 +
 src/bin/psql/tab-complete.c                   |   10 +-
 src/include/nodes/parsenodes.h                |    1 +
 src/include/parser/kwlist.h                   |    1 +
 src/include/partitioning/partbounds.h         |    5 +
 src/include/utils/ruleutils.h                 |    2 +
 .../isolation/expected/partition-split.out    |  190 ++
 src/test/isolation/isolation_schedule         |    1 +
 src/test/isolation/specs/partition-split.spec |   54 +
 .../test_ddl_deparse/test_ddl_deparse.c       |    3 +
 src/test/regress/expected/partition_split.out | 1589 +++++++++++++++++
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/partition_split.sql      |  962 ++++++++++
 src/tools/pgindent/typedefs.list              |    1 +
 20 files changed, 4109 insertions(+), 17 deletions(-)
 create mode 100644 src/test/isolation/expected/partition-split.out
 create mode 100644 src/test/isolation/specs/partition-split.spec
 create mode 100644 src/test/regress/expected/partition_split.out
 create mode 100644 src/test/regress/sql/partition_split.sql

diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 2770e81656..5f481dda3a 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -4374,6 +4374,25 @@ ALTER TABLE measurement
                       measurement_y2006m03) INTO measurement_y2006q1;
 </programlisting>
     </para>
+
+    <para>
+     Similarly to merging multiple table partitions, there is an option for
+     splitting a single partition into multiple using the
+     <link linkend="sql-altertable-split-partition"><command>ALTER TABLE ... SPLIT PARTITION</command></link>.
+     This feature could come in handy when one partition grows too big
+     and needs to be split into multiple.  It's important to note that
+     this operation is not supported for hash-partitioned tables and acquires
+     an <literal>ACCESS EXCLUSIVE</literal> lock, which could impact high-load
+     systems due to the lock's restrictive nature.  For example, we can split
+     the quarter partition back to monthly partitions:
+<programlisting>
+ALTER TABLE measurement SPLIT PARTITION measurement_y2006q1 INTO
+   (PARTITION measurement_y2006m01 FOR VALUES FROM ('2006-01-01') TO ('2006-02-01'),
+    PARTITION measurement_y2006m02 FOR VALUES FROM ('2006-02-01') TO ('2006-03-01'),
+    PARTITION measurement_y2006m03 FOR VALUES FROM ('2006-03-01') TO ('2006-04-01'));
+</programlisting>
+    </para>
+
    </sect3>
 
    <sect3 id="ddl-partitioning-declarative-limitations">
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index b99d4972ac..6a2822adad 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -37,6 +37,10 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
     ATTACH PARTITION <replaceable class="parameter">partition_name</replaceable> { FOR VALUES <replaceable class="parameter">partition_bound_spec</replaceable> | DEFAULT }
 ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
     DETACH PARTITION <replaceable class="parameter">partition_name</replaceable> [ CONCURRENTLY | FINALIZE ]
+ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
+    SPLIT PARTITION <replaceable class="parameter">partition_name</replaceable> INTO
+        (PARTITION <replaceable class="parameter">partition_name1</replaceable> { FOR VALUES <replaceable class="parameter">partition_bound_spec</replaceable> | DEFAULT },
+         PARTITION <replaceable class="parameter">partition_name2</replaceable> { FOR VALUES <replaceable class="parameter">partition_bound_spec</replaceable> | DEFAULT } [, ...])
 ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
     MERGE PARTITIONS (<replaceable class="parameter">partition_name1</replaceable>, <replaceable class="parameter">partition_name2</replaceable> [, ...])
         INTO <replaceable class="parameter">partition_name</replaceable>
@@ -1120,6 +1124,58 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-altertable-split-partition">
+    <term><literal>SPLIT PARTITION <replaceable class="parameter">partition_name</replaceable> INTO (PARTITION <replaceable class="parameter">partition_name1</replaceable> { FOR VALUES <replaceable class="parameter">partition_bound_spec</replaceable> | DEFAULT }, PARTITION <replaceable class="parameter">partition_name2</replaceable> { FOR VALUES <replaceable class="parameter">partition_bound_spec</replaceable> | DEFAULT } [, ...])</literal></term>
+
+    <listitem>
+     <para>
+      This form splits a single partition of the target table. Hash-partitioning
+      is not supported. Bounds of new partitions should not overlap with new and
+      existing partitions (except <replaceable class="parameter">partition_name</replaceable>).
+      If the split partition is a DEFAULT partition, one of the new partitions must be DEFAULT.
+      In case one of the new partitions or one of existing partitions is DEFAULT,
+      new partitions <replaceable class="parameter">partition_name1</replaceable>,
+      <replaceable class="parameter">partition_name2</replaceable>, ... can have spaces
+      between partitions bounds.  If the partitioned table does not have a DEFAULT
+      partition, the DEFAULT partition can be defined as one of the new partitions.
+     </para>
+     <para>
+      In case new partitions do not contain a DEFAULT partition and the partitioned table
+      does not have a DEFAULT partition, the following must be true: sum bounds of
+      new partitions <replaceable class="parameter">partition_name1</replaceable>,
+      <replaceable class="parameter">partition_name2</replaceable>, ... should be
+      equal to bound of split partition <replaceable class="parameter">partition_name</replaceable>.
+      One of the new partitions <replaceable class="parameter">partition_name1</replaceable>,
+      <replaceable class="parameter">partition_name2</replaceable>, ... can have
+      the same name as split partition <replaceable class="parameter">partition_name</replaceable>
+      (this is suitable in case of splitting a DEFAULT partition: we split it, but after
+      splitting we have a partition with the same name).
+      Only simple, non-partitioned partition can be split.
+     </para>
+     <para>
+      The new partitions will be created the same as tables created with the
+      SQL command <literal>CREATE TABLE <replaceable class="parameter">partition_nameN</replaceable> (LIKE <replaceable class="parameter">name</replaceable> INCLUDING ALL EXCLUDING INDEXES EXCLUDING IDENTITY EXCLUDING STATISTICS)</literal>.
+      The indexes and identity are created later, after moving the data
+      into the new partitions.
+      Extended statistics aren't copied from the parent table, for consistency with
+      <command>CREATE TABLE PARTITION OF</command>.
+
+      New partitions will have the same table access method as the parent.
+      If the parent table is persistent then new partitions are created
+      persistent.  If the parent table is temporary then new partitions
+      are also created temporary.  New partitions will also be created in
+      the same tablespace as the parent.
+     </para>
+     <note>
+      <para>
+       This command acquires an <literal>ACCESS EXCLUSIVE</literal> lock.
+       This is a significant limitation, which limits the usage of this
+       command with large partitioned tables under a high load.
+      </para>
+     </note>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-altertable-merge-partitions">
     <term><literal>MERGE PARTITIONS (<replaceable class="parameter">partition_name1</replaceable>, <replaceable class="parameter">partition_name2</replaceable> [, ...]) INTO <replaceable class="parameter">partition_name</replaceable></literal></term>
 
@@ -1200,7 +1256,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
    All the forms of ALTER TABLE that act on a single table, except
    <literal>RENAME</literal>, <literal>SET SCHEMA</literal>,
    <literal>ATTACH PARTITION</literal>, <literal>DETACH PARTITION</literal>,
-   and <literal>MERGE PARTITIONS</literal> can be combined into
+   <literal>SPLIT PARTITION</literal>, and <literal>MERGE PARTITIONS</literal>
+   can be combined into
    a list of multiple alterations to be applied together.  For example, it
    is possible to add several columns and/or alter the type of several
    columns in a single command.  This is particularly useful with large
@@ -1444,7 +1501,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       <listitem>
        <para>
         The name of the table to attach as a new partition or to detach from this table,
-        or the name of the new merged partition.
+        or the name of split partition, or the name of the new merged partition.
        </para>
       </listitem>
      </varlistentry>
@@ -1860,6 +1917,24 @@ ALTER TABLE measurement
     DETACH PARTITION measurement_y2015m12;
 </programlisting></para>
 
+  <para>
+   To split a single partition of the range-partitioned table:
+<programlisting>
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2023 INTO
+   (PARTITION sales_feb2023 FOR VALUES FROM ('2023-02-01') TO ('2023-03-01'),
+    PARTITION sales_mar2023 FOR VALUES FROM ('2023-03-01') TO ('2023-04-01'),
+    PARTITION sales_apr2023 FOR VALUES FROM ('2023-04-01') TO ('2023-05-01'));
+</programlisting></para>
+
+  <para>
+   To split a single partition of the list-partitioned table:
+<programlisting>
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+   (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'),
+    PARTITION sales_east FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok'),
+    PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'));
+</programlisting></para>
+
   <para>
    To merge several partitions into one partition of the target table:
 <programlisting>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 4d24278495..55db12ef35 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -657,6 +657,9 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, const char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);
 
+static void ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab,
+								 Relation rel, PartitionCmd *cmd,
+								 AlterTableUtilityContext *context);
 static void ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
 								  PartitionCmd *cmd, AlterTableUtilityContext *context);
 
@@ -4678,6 +4681,10 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = AccessShareLock;
 				break;
 
+			case AT_SplitPartition:
+				cmd_lockmode = AccessExclusiveLock;
+				break;
+
 			case AT_MergePartitions:
 				cmd_lockmode = AccessExclusiveLock;
 				break;
@@ -5099,6 +5106,11 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* No command-specific prep needed */
 			pass = AT_PASS_MISC;
 			break;
+		case AT_SplitPartition:
+			ATSimplePermissions(cmd->subtype, rel, ATT_TABLE);
+			/* No command-specific prep needed */
+			pass = AT_PASS_MISC;
+			break;
 		case AT_MergePartitions:
 			ATSimplePermissions(cmd->subtype, rel, ATT_TABLE);
 			/* No command-specific prep needed */
@@ -5500,6 +5512,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 		case AT_DetachPartitionFinalize:
 			address = ATExecDetachPartitionFinalize(rel, ((PartitionCmd *) cmd->def)->name);
 			break;
+		case AT_SplitPartition:
+			cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode,
+									  cur_pass, context);
+			Assert(cmd != NULL);
+			Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
+			ATExecSplitPartition(wqueue, tab, rel, (PartitionCmd *) cmd->def,
+								 context);
+			break;
 		case AT_MergePartitions:
 			cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode,
 									  cur_pass, context);
@@ -6496,6 +6516,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
 			return "DETACH PARTITION";
 		case AT_DetachPartitionFinalize:
 			return "DETACH PARTITION ... FINALIZE";
+		case AT_SplitPartition:
+			return "SPLIT PARTITION";
 		case AT_MergePartitions:
 			return "MERGE PARTITIONS";
 		case AT_AddIdentity:
@@ -20056,6 +20078,256 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 	return cstorage;
 }
 
+/*
+ * Struct with context of new partition for inserting rows from split partition
+ */
+typedef struct SplitPartitionContext
+{
+	ExprState  *partqualstate;	/* expression for checking slot for partition
+								 * (NULL for DEFAULT partition) */
+	BulkInsertState bistate;	/* state of bulk inserts for partition */
+	TupleTableSlot *dstslot;	/* slot for inserting row into partition */
+	Relation	partRel;		/* relation for partition */
+} SplitPartitionContext;
+
+
+/*
+ * createSplitPartitionContext: create context for partition and fill it
+ */
+static SplitPartitionContext *
+createSplitPartitionContext(Relation partRel)
+{
+	SplitPartitionContext *pc;
+
+	pc = (SplitPartitionContext *) palloc0(sizeof(SplitPartitionContext));
+	pc->partRel = partRel;
+
+	/*
+	 * Prepare a BulkInsertState for table_tuple_insert. The FSM is empty, so
+	 * don't bother using it.
+	 */
+	pc->bistate = GetBulkInsertState();
+
+	/* Create tuple slot for new partition. */
+	pc->dstslot = MakeSingleTupleTableSlot(RelationGetDescr(pc->partRel),
+										   table_slot_callbacks(pc->partRel));
+	ExecStoreAllNullTuple(pc->dstslot);
+
+	return pc;
+}
+
+/*
+ * deleteSplitPartitionContext: delete context for partition
+ */
+static void
+deleteSplitPartitionContext(SplitPartitionContext *pc, int ti_options)
+{
+	ExecDropSingleTupleTableSlot(pc->dstslot);
+	FreeBulkInsertState(pc->bistate);
+
+	table_finish_bulk_insert(pc->partRel, ti_options);
+
+	pfree(pc);
+}
+
+/*
+ * moveSplitTableRows: scan split partition (splitRel) of partitioned table
+ * (rel) and move rows into new partitions.
+ *
+ * New partitions description:
+ * partlist: list of pointers to SinglePartitionSpec structures.
+ * newPartRels: list of Relations.
+ * defaultPartOid: oid of DEFAULT partition, for table rel.
+ */
+static void
+moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPartRels, Oid defaultPartOid)
+{
+	/* The FSM is empty, so don't bother using it. */
+	int			ti_options = TABLE_INSERT_SKIP_FSM;
+	CommandId	mycid;
+	EState	   *estate;
+	ListCell   *listptr,
+			   *listptr2;
+	TupleTableSlot *srcslot;
+	ExprContext *econtext;
+	TableScanDesc scan;
+	Snapshot	snapshot;
+	MemoryContext oldCxt;
+	List	   *partContexts = NIL;
+	TupleConversionMap *tuple_map;
+	SplitPartitionContext *defaultPartCtx = NULL,
+			   *pc;
+	bool		isOldDefaultPart = false;
+
+	mycid = GetCurrentCommandId(true);
+
+	estate = CreateExecutorState();
+
+	forboth(listptr, partlist, listptr2, newPartRels)
+	{
+		SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
+
+		pc = createSplitPartitionContext((Relation) lfirst(listptr2));
+
+		if (sps->bound->is_default)
+		{
+			/* We should not create constraint for detached DEFAULT partition. */
+			defaultPartCtx = pc;
+		}
+		else
+		{
+			List	   *partConstraint;
+
+			/* Build expression execution states for partition check quals. */
+			partConstraint = get_qual_from_partbound(rel, sps->bound);
+			partConstraint =
+				(List *) eval_const_expressions(NULL,
+												(Node *) partConstraint);
+			/* Make boolean expression for ExecCheck(). */
+			partConstraint = list_make1(make_ands_explicit(partConstraint));
+
+			/*
+			 * Map the vars in the constraint expression from rel's attnos to
+			 * splitRel's.
+			 */
+			partConstraint = map_partition_varattnos(partConstraint,
+													 1, splitRel, rel);
+
+			pc->partqualstate =
+				ExecPrepareExpr((Expr *) linitial(partConstraint), estate);
+			Assert(pc->partqualstate != NULL);
+		}
+
+		/* Store partition context into list. */
+		partContexts = lappend(partContexts, pc);
+	}
+
+	/*
+	 * Create partition context for DEFAULT partition. We can insert values
+	 * into this partition in case spaces with values between new partitions.
+	 */
+	if (!defaultPartCtx && OidIsValid(defaultPartOid))
+	{
+		/* Indicate that we allocate context for old DEFAULT partition */
+		isOldDefaultPart = true;
+		defaultPartCtx = createSplitPartitionContext(table_open(defaultPartOid, AccessExclusiveLock));
+	}
+
+	econtext = GetPerTupleExprContext(estate);
+
+	/* Create necessary tuple slot. */
+	srcslot = MakeSingleTupleTableSlot(RelationGetDescr(splitRel),
+									   table_slot_callbacks(splitRel));
+
+	/*
+	 * Map computing for moving attributes of split partition to new partition
+	 * (for first new partition, but other new partitions can use the same
+	 * map).
+	 */
+	pc = (SplitPartitionContext *) lfirst(list_head(partContexts));
+	tuple_map = convert_tuples_by_name(RelationGetDescr(splitRel),
+									   RelationGetDescr(pc->partRel));
+
+	/* Scan through the rows. */
+	snapshot = RegisterSnapshot(GetLatestSnapshot());
+	scan = table_beginscan(splitRel, snapshot, 0, NULL);
+
+	/*
+	 * Switch to per-tuple memory context and reset it for each tuple
+	 * produced, so we don't leak memory.
+	 */
+	oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+	while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot))
+	{
+		bool		found = false;
+		TupleTableSlot *insertslot;
+
+		/* Extract data from old tuple. */
+		slot_getallattrs(srcslot);
+
+		econtext->ecxt_scantuple = srcslot;
+
+		/* Search partition for current slot srcslot. */
+		foreach(listptr, partContexts)
+		{
+			pc = (SplitPartitionContext *) lfirst(listptr);
+
+			if (pc->partqualstate /* skip DEFAULT partition */ &&
+				ExecCheck(pc->partqualstate, econtext))
+			{
+				found = true;
+				break;
+			}
+			ResetExprContext(econtext);
+		}
+		if (!found)
+		{
+			/* Use DEFAULT partition if it exists. */
+			if (defaultPartCtx)
+				pc = defaultPartCtx;
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_CHECK_VIOLATION),
+						 errmsg("can not find partition for split partition row"),
+						 errtable(splitRel)));
+		}
+
+		if (tuple_map)
+		{
+			/* Need to use map to copy attributes. */
+			insertslot = execute_attr_map_slot(tuple_map->attrMap, srcslot, pc->dstslot);
+		}
+		else
+		{
+			/* Copy attributes directly. */
+			insertslot = pc->dstslot;
+
+			ExecClearTuple(insertslot);
+
+			memcpy(insertslot->tts_values, srcslot->tts_values,
+				   sizeof(Datum) * srcslot->tts_nvalid);
+			memcpy(insertslot->tts_isnull, srcslot->tts_isnull,
+				   sizeof(bool) * srcslot->tts_nvalid);
+
+			ExecStoreVirtualTuple(insertslot);
+		}
+
+		/* Write the tuple out to the new relation. */
+		table_tuple_insert(pc->partRel, insertslot, mycid,
+						   ti_options, pc->bistate);
+
+		ResetExprContext(econtext);
+
+		CHECK_FOR_INTERRUPTS();
+	}
+
+	MemoryContextSwitchTo(oldCxt);
+
+	table_endscan(scan);
+	UnregisterSnapshot(snapshot);
+
+	if (tuple_map)
+		free_conversion_map(tuple_map);
+
+	ExecDropSingleTupleTableSlot(srcslot);
+
+	FreeExecutorState(estate);
+
+	foreach(listptr, partContexts)
+		deleteSplitPartitionContext((SplitPartitionContext *) lfirst(listptr), ti_options);
+
+	/* Need to close table and free buffers for DEFAULT partition. */
+	if (isOldDefaultPart)
+	{
+		Relation	defaultPartRel = defaultPartCtx->partRel;
+
+		deleteSplitPartitionContext(defaultPartCtx, ti_options);
+		/* Keep the lock until commit. */
+		table_close(defaultPartRel, NoLock);
+	}
+}
+
 /*
  * createPartitionTable: create table for a new partition with given name
  * (newPartName) like table (modelRel)
@@ -20160,6 +20432,132 @@ createPartitionTable(RangeVar *newPartName, Relation modelRel,
 	return newRel;
 }
 
+/*
+ * ALTER TABLE <name> SPLIT PARTITION <partition-name> INTO <partition-list>
+ */
+static void
+ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
+					 PartitionCmd *cmd, AlterTableUtilityContext *context)
+{
+	Relation	splitRel;
+	Oid			splitRelOid;
+	char		relname[NAMEDATALEN];
+	Oid			namespaceId;
+	ListCell   *listptr,
+			   *listptr2;
+	bool		isSameName = false;
+	char		tmpRelName[NAMEDATALEN];
+	List	   *newPartRels = NIL;
+	ObjectAddress object;
+	Oid			defaultPartOid;
+
+	defaultPartOid = get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true));
+
+	/*
+	 * We are going to detach and remove this partition: need to use exclusive
+	 * lock for preventing DML-queries to the partition.
+	 */
+	splitRel = table_openrv(cmd->name, AccessExclusiveLock);
+
+	splitRelOid = RelationGetRelid(splitRel);
+
+	/* Check descriptions of new partitions. */
+	foreach(listptr, cmd->partlist)
+	{
+		Oid			existing_relid;
+		SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
+
+		strlcpy(relname, sps->name->relname, NAMEDATALEN);
+
+		/*
+		 * Look up the namespace in which we are supposed to create the
+		 * partition, check we have permission to create there, lock it
+		 * against concurrent drop, and mark stmt->relation as
+		 * RELPERSISTENCE_TEMP if a temporary namespace is selected.
+		 */
+		sps->name->relpersistence = rel->rd_rel->relpersistence;
+		namespaceId =
+			RangeVarGetAndCheckCreationNamespace(sps->name, NoLock, NULL);
+
+		/*
+		 * This would fail later on anyway if the relation already exists. But
+		 * by catching it here we can emit a nicer error message.
+		 */
+		existing_relid = get_relname_relid(relname, namespaceId);
+		if (existing_relid == splitRelOid && !isSameName)
+			/* One new partition can have the same name as split partition. */
+			isSameName = true;
+		else if (existing_relid != InvalidOid)
+			ereport(ERROR,
+					(errcode(ERRCODE_DUPLICATE_TABLE),
+					 errmsg("relation \"%s\" already exists", relname)));
+	}
+
+	/* Detach split partition. */
+	RemoveInheritance(splitRel, rel, false);
+	/* Do the final part of detaching. */
+	DetachPartitionFinalize(rel, splitRel, false, defaultPartOid);
+
+	/*
+	 * If new partition has the same name as split partition then we should
+	 * rename split partition for reusing name.
+	 */
+	if (isSameName)
+	{
+		/*
+		 * We must bump the command counter to make the split partition tuple
+		 * visible for renaming.
+		 */
+		CommandCounterIncrement();
+		/* Rename partition. */
+		sprintf(tmpRelName, "split-%u-%X-tmp", RelationGetRelid(rel), MyProcPid);
+		RenameRelationInternal(splitRelOid, tmpRelName, false, false);
+
+		/*
+		 * We must bump the command counter to make the split partition tuple
+		 * visible after renaming.
+		 */
+		CommandCounterIncrement();
+	}
+
+	/* Create new partitions (like split partition), without indexes. */
+	foreach(listptr, cmd->partlist)
+	{
+		SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
+		Relation	newPartRel;
+
+		newPartRel = createPartitionTable(sps->name, rel, context);
+		newPartRels = lappend(newPartRels, newPartRel);
+	}
+
+	/* Copy data from split partition to new partitions. */
+	moveSplitTableRows(rel, splitRel, cmd->partlist, newPartRels, defaultPartOid);
+	/* Keep the lock until commit. */
+	table_close(splitRel, NoLock);
+
+	/* Attach new partitions to partitioned table. */
+	forboth(listptr, cmd->partlist, listptr2, newPartRels)
+	{
+		SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
+		Relation	newPartRel = (Relation) lfirst(listptr2);
+
+		/*
+		 * wqueue = NULL: verification for each cloned constraint is not
+		 * needed.
+		 */
+		attachPartitionTable(NULL, rel, newPartRel, sps->bound);
+		/* Keep the lock until commit. */
+		table_close(newPartRel, NoLock);
+	}
+
+	/* Drop split partition. */
+	object.classId = RelationRelationId;
+	object.objectId = splitRelOid;
+	object.objectSubId = 0;
+	/* Probably DROP_CASCADE is not needed. */
+	performDeletion(&object, DROP_RESTRICT, 0);
+}
+
 /*
  * moveMergedTablesRows: scan partitions to be merged (mergingPartitionsList)
  * of the partitioned table (rel) and move rows into the new partition
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d85c74cb7c..c3f25582c3 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -269,6 +269,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	PartitionElem *partelem;
 	PartitionSpec *partspec;
 	PartitionBoundSpec *partboundspec;
+	SinglePartitionSpec *singlepartspec;
 	RoleSpec   *rolespec;
 	PublicationObjSpec *publicationobjectspec;
 	struct SelectLimit *selectlimit;
@@ -645,6 +646,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <partelem>	part_elem
 %type <list>		part_params
 %type <partboundspec> PartitionBoundSpec
+%type <singlepartspec>	SinglePartitionSpec
+%type <list>		partitions_list
 %type <list>		hash_partbound
 %type <defelt>		hash_partbound_elem
 
@@ -775,7 +778,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	SAVEPOINT SCALAR SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SELECT
 	SEQUENCE SEQUENCES
 	SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW
-	SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SOURCE SQL_P STABLE STANDALONE_P
+	SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SPLIT SOURCE SQL_P STABLE STANDALONE_P
 	START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRING_P STRIP_P
 	SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER
 
@@ -2308,6 +2311,23 @@ alter_table_cmds:
 			| alter_table_cmds ',' alter_table_cmd	{ $$ = lappend($1, $3); }
 		;
 
+partitions_list:
+			SinglePartitionSpec							{ $$ = list_make1($1); }
+			| partitions_list ',' SinglePartitionSpec	{ $$ = lappend($1, $3); }
+		;
+
+SinglePartitionSpec:
+			PARTITION qualified_name PartitionBoundSpec
+				{
+					SinglePartitionSpec *n = makeNode(SinglePartitionSpec);
+
+					n->name = $2;
+					n->bound = $3;
+
+					$$ = n;
+				}
+		;
+
 partition_cmd:
 			/* ALTER TABLE <name> ATTACH PARTITION <table_name> FOR VALUES */
 			ATTACH PARTITION qualified_name PartitionBoundSpec
@@ -2352,6 +2372,20 @@ partition_cmd:
 					n->def = (Node *) cmd;
 					$$ = (Node *) n;
 				}
+			/* ALTER TABLE <name> SPLIT PARTITION <partition_name> INTO () */
+			| SPLIT PARTITION qualified_name INTO '(' partitions_list ')'
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					PartitionCmd *cmd = makeNode(PartitionCmd);
+
+					n->subtype = AT_SplitPartition;
+					cmd->name = $3;
+					cmd->bound = NULL;
+					cmd->partlist = $6;
+					cmd->concurrent = false;
+					n->def = (Node *) cmd;
+					$$ = (Node *) n;
+				}
 			/* ALTER TABLE <name> MERGE PARTITIONS () INTO <partition_name> */
 			| MERGE PARTITIONS '(' qualified_name_list ')' INTO qualified_name
 				{
@@ -17777,6 +17811,7 @@ unreserved_keyword:
 			| SKIP
 			| SNAPSHOT
 			| SOURCE
+			| SPLIT
 			| SQL_P
 			| STABLE
 			| STANDALONE_P
@@ -18413,6 +18448,7 @@ bare_label_keyword:
 			| SNAPSHOT
 			| SOME
 			| SOURCE
+			| SPLIT
 			| SQL_P
 			| STABLE
 			| STANDALONE_P
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 63ea2ec5ea..d5c2b2ff0b 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -136,7 +136,7 @@ static void transformConstraintAttrs(CreateStmtContext *cxt,
 									 List *constraintList);
 static void transformColumnType(CreateStmtContext *cxt, ColumnDef *column);
 static void setSchemaName(const char *context_schema, char **stmt_schema_name);
-static void transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd);
+static void transformPartitionCmd(CreateStmtContext *cxt, PartitionBoundSpec *bound);
 static List *transformPartitionRangeBounds(ParseState *pstate, List *blist,
 										   Relation parent);
 static void validateInfiniteBounds(ParseState *pstate, List *blist);
@@ -3270,6 +3270,45 @@ checkPartition(Relation rel, Oid partRelOid)
 	relation_close(partRel, AccessShareLock);
 }
 
+/*
+ * transformPartitionCmdForSplit
+ *		Analyze the ALTER TABLE ... SPLIT PARTITION command
+ *
+ * For each new partition sps->bound is set to the transformed value of bound.
+ * Does checks for bounds of new partitions.
+ */
+static void
+transformPartitionCmdForSplit(CreateStmtContext *cxt, PartitionCmd *partcmd)
+{
+	Relation	parent = cxt->rel;
+	Oid			splitPartOid;
+	ListCell   *listptr;
+
+	if (parent->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("\"%s\" is not a partitioned table", RelationGetRelationName(parent))));
+
+	/* Transform partition bounds for all partitions in the list: */
+	foreach(listptr, partcmd->partlist)
+	{
+		SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
+
+		cxt->partbound = NULL;
+		transformPartitionCmd(cxt, sps->bound);
+		/* Assign transformed value of the partition bound. */
+		sps->bound = cxt->partbound;
+	}
+
+	splitPartOid = RangeVarGetRelid(partcmd->name, NoLock, false);
+
+	checkPartition(parent, splitPartOid);
+
+	/* Then we should check partitions with transformed bounds. */
+	check_partitions_for_split(parent, splitPartOid, partcmd->name, partcmd->partlist, cxt->pstate);
+}
+
+
 /*
  * transformPartitionCmdForMerge
  *		Analyze the ALTER TABLE ... MERGE PARTITIONS command
@@ -3615,7 +3654,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 				{
 					PartitionCmd *partcmd = (PartitionCmd *) cmd->def;
 
-					transformPartitionCmd(&cxt, partcmd);
+					transformPartitionCmd(&cxt, partcmd->bound);
 					/* assign transformed value of the partition bound */
 					partcmd->bound = cxt.partbound;
 				}
@@ -3623,6 +3662,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 				newcmds = lappend(newcmds, cmd);
 				break;
 
+			case AT_SplitPartition:
 			case AT_MergePartitions:
 				{
 					PartitionCmd *partcmd = (PartitionCmd *) cmd->def;
@@ -3631,7 +3671,11 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 						ereport(ERROR,
 								(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 								 errmsg("list of new partitions should contain at least two items")));
-					transformPartitionCmdForMerge(&cxt, partcmd);
+
+					if (cmd->subtype == AT_SplitPartition)
+						transformPartitionCmdForSplit(&cxt, partcmd);
+					else
+						transformPartitionCmdForMerge(&cxt, partcmd);
 					newcmds = lappend(newcmds, cmd);
 					break;
 				}
@@ -4026,13 +4070,13 @@ setSchemaName(const char *context_schema, char **stmt_schema_name)
 
 /*
  * transformPartitionCmd
- *		Analyze the ATTACH/DETACH PARTITION command
+ *		Analyze the ATTACH/DETACH/SPLIT PARTITION command
  *
- * In case of the ATTACH PARTITION command, cxt->partbound is set to the
- * transformed value of cmd->bound.
+ * In case of the ATTACH/SPLIT PARTITION command, cxt->partbound is set to the
+ * transformed value of bound.
  */
 static void
-transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
+transformPartitionCmd(CreateStmtContext *cxt, PartitionBoundSpec *bound)
 {
 	Relation	parentRel = cxt->rel;
 
@@ -4041,9 +4085,9 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 		case RELKIND_PARTITIONED_TABLE:
 			/* transform the partition bound, if any */
 			Assert(RelationGetPartitionKey(parentRel) != NULL);
-			if (cmd->bound != NULL)
+			if (bound != NULL)
 				cxt->partbound = transformPartitionBound(cxt->pstate, parentRel,
-														 cmd->bound);
+														 bound);
 			break;
 		case RELKIND_PARTITIONED_INDEX:
 
@@ -4051,7 +4095,7 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 			 * A partitioned index cannot have a partition bound set.  ALTER
 			 * INDEX prevents that with its grammar, but not ALTER TABLE.
 			 */
-			if (cmd->bound != NULL)
+			if (bound != NULL)
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("\"%s\" is not a partitioned table",
diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index 6cb32d57ef..4eda59767c 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -5041,10 +5041,69 @@ check_two_partitions_bounds_range(Relation parent,
 	}
 }
 
+/*
+ * check_partitions_not_overlap_list
+ *
+ * (function for BY LIST partitioning)
+ *
+ * This is a helper function for check_partitions_for_split().
+ * Checks that the values of the new partitions do not overlap.
+ *
+ * parent:	partitioned table
+ * parts:	array of SinglePartitionSpec structs with info about split partitions
+ * nparts:	size of array "parts"
+ */
+static void
+check_partitions_not_overlap_list(Relation parent,
+								  SinglePartitionSpec **parts,
+								  int nparts,
+								  ParseState *pstate)
+{
+	PartitionKey key PG_USED_FOR_ASSERTS_ONLY = RelationGetPartitionKey(parent);
+	int			overlap_location = -1;
+	int			i,
+				j;
+	SinglePartitionSpec *sps1,
+			   *sps2;
+	List	   *overlap;
+
+	Assert(key->strategy == PARTITION_STRATEGY_LIST);
+
+	for (i = 0; i < nparts; i++)
+	{
+		sps1 = parts[i];
+
+		for (j = i + 1; j < nparts; j++)
+		{
+			sps2 = parts[j];
+
+			/*
+			 * Calculate intersection between values of two partitions.
+			 */
+			overlap = list_intersection(sps1->bound->listdatums,
+										sps2->bound->listdatums);
+			if (list_length(overlap) > 0)
+			{
+				Const	   *val = (Const *) lfirst(list_head(overlap));
+
+				overlap_location = val->location;
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("new partition \"%s\" would overlap with another new partition \"%s\"",
+								sps1->name->relname, sps2->name->relname),
+						 parser_errposition(pstate, overlap_location)));
+			}
+		}
+	}
+}
+
 /*
  * get_partition_bound_spec
  *
  * Returns description of partition with Oid "partOid" and name "name".
+ *
+ * partOid:	partition Oid
+ * name:	partition name
  */
 static PartitionBoundSpec *
 get_partition_bound_spec(Oid partOid, RangeVar *name)
@@ -5077,6 +5136,636 @@ get_partition_bound_spec(Oid partOid, RangeVar *name)
 	return boundspec;
 }
 
+/*
+ * check_partition_bounds_for_split_range
+ *
+ * (function for BY RANGE partitioning)
+ *
+ * Checks that bounds of new partition "spec" are inside bounds of split
+ * partition (with Oid splitPartOid). If first=true (this means that "spec" is
+ * the first of new partitions) then lower bound of "spec" should be equal (or
+ * greater than or equal in case defaultPart=true) to lower bound of split
+ * partition. If last=true (this means that "spec" is the last of new
+ * partitions) then upper bound of "spec" should be equal (or less than or
+ * equal in case defaultPart=true) to upper bound of split partition.
+ *
+ * parent:			partitioned table
+ * relname:			name of the new partition
+ * spec:			bounds specification of the new partition
+ * splitPartOid:	split partition Oid
+ * splitPartName:	split partition name
+ * first:			true in case new partition "spec" is first of new partitions
+ * last:			true in case new partition "spec" is last of new partitions
+ * defaultPart:		true in case partitioned table has DEFAULT partition
+ * pstate:			pointer to ParseState struct for determine error position
+ */
+static void
+check_partition_bounds_for_split_range(Relation parent,
+									   char *relname,
+									   PartitionBoundSpec *spec,
+									   Oid splitPartOid,
+									   RangeVar *splitPartName,
+									   bool first,
+									   bool last,
+									   bool defaultPart,
+									   ParseState *pstate)
+{
+	PartitionKey key = RelationGetPartitionKey(parent);
+	PartitionRangeBound *lower,
+			   *upper;
+	int			cmpval;
+
+	Assert(key->strategy == PARTITION_STRATEGY_RANGE);
+	Assert(spec->strategy == PARTITION_STRATEGY_RANGE);
+
+	lower = make_one_partition_rbound(key, -1, spec->lowerdatums, true);
+	upper = make_one_partition_rbound(key, -1, spec->upperdatums, false);
+
+	/*
+	 * First check if the resulting range would be empty with specified lower
+	 * and upper bounds.  partition_rbound_cmp cannot return zero here, since
+	 * the lower-bound flags are different.
+	 */
+	cmpval = partition_rbound_cmp(key->partnatts,
+								  key->partsupfunc,
+								  key->partcollation,
+								  lower->datums, lower->kind,
+								  true, upper);
+	Assert(cmpval != 0);
+	if (cmpval > 0)
+	{
+		/* Point to problematic key in the lower datums list. */
+		PartitionRangeDatum *datum = list_nth(spec->lowerdatums, cmpval - 1);
+
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("empty range bound specified for partition \"%s\"",
+						relname),
+				 errdetail("Specified lower bound %s is greater than or equal to upper bound %s.",
+						   get_range_partbound_string(spec->lowerdatums),
+						   get_range_partbound_string(spec->upperdatums)),
+				 parser_errposition(pstate, datum->location)));
+	}
+
+	/* Need to check first and last partitions (from set of new partitions) */
+	if (first || last)
+	{
+		PartitionBoundSpec *split_spec = get_partition_bound_spec(splitPartOid, splitPartName);
+		PartitionRangeDatum *datum;
+
+		if (first)
+		{
+			PartitionRangeBound *split_lower;
+
+			split_lower = make_one_partition_rbound(key, -1, split_spec->lowerdatums, true);
+
+			cmpval = partition_rbound_cmp(key->partnatts,
+										  key->partsupfunc,
+										  key->partcollation,
+										  lower->datums, lower->kind,
+										  true, split_lower);
+
+			/*
+			 * Lower bound of "spec" should be equal (or greater than or equal
+			 * in case defaultPart=true) to lower bound of split partition.
+			 */
+			if (!defaultPart)
+			{
+				if (cmpval != 0)
+				{
+					datum = list_nth(spec->lowerdatums, abs(cmpval) - 1);
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							 errmsg("lower bound of partition \"%s\" is not equal to lower bound of split partition",
+									relname),
+							 parser_errposition(pstate, datum->location)));
+				}
+			}
+			else
+			{
+				if (cmpval < 0)
+				{
+					datum = list_nth(spec->lowerdatums, abs(cmpval) - 1);
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							 errmsg("lower bound of partition \"%s\" is less than lower bound of split partition",
+									relname),
+							 parser_errposition(pstate, datum->location)));
+				}
+			}
+		}
+		else
+		{
+			PartitionRangeBound *split_upper;
+
+			split_upper = make_one_partition_rbound(key, -1, split_spec->upperdatums, false);
+
+			cmpval = partition_rbound_cmp(key->partnatts,
+										  key->partsupfunc,
+										  key->partcollation,
+										  upper->datums, upper->kind,
+										  false, split_upper);
+
+			/*
+			 * Upper bound of "spec" should be equal (or less than or equal in
+			 * case defaultPart=true) to upper bound of split partition.
+			 */
+			if (!defaultPart)
+			{
+				if (cmpval != 0)
+				{
+					datum = list_nth(spec->upperdatums, abs(cmpval) - 1);
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							 errmsg("upper bound of partition \"%s\" is not equal to upper bound of split partition",
+									relname),
+							 parser_errposition(pstate, datum->location)));
+				}
+			}
+			else
+			{
+				if (cmpval > 0)
+				{
+					datum = list_nth(spec->upperdatums, abs(cmpval) - 1);
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							 errmsg("upper bound of partition \"%s\" is greater than upper bound of split partition",
+									relname),
+							 parser_errposition(pstate, datum->location)));
+				}
+			}
+		}
+	}
+}
+
+/*
+ * check_partition_bounds_for_split_list
+ *
+ * (function for BY LIST partitioning)
+ *
+ * Checks that bounds of new partition are inside bounds of split partition
+ * (with Oid splitPartOid).
+ *
+ * parent:			partitioned table
+ * relname:			name of the new partition
+ * spec:			bounds specification of the new partition
+ * splitPartOid:	split partition Oid
+ * pstate:			pointer to ParseState struct for determine error position
+ */
+static void
+check_partition_bounds_for_split_list(Relation parent, char *relname,
+									  PartitionBoundSpec *spec,
+									  Oid splitPartOid,
+									  ParseState *pstate)
+{
+	PartitionKey key = RelationGetPartitionKey(parent);
+	PartitionDesc partdesc = RelationGetPartitionDesc(parent, false);
+	PartitionBoundInfo boundinfo = partdesc->boundinfo;
+	int			with = -1;
+	bool		overlap = false;
+	int			overlap_location = -1;
+	ListCell   *cell;
+
+	Assert(key->strategy == PARTITION_STRATEGY_LIST);
+	Assert(spec->strategy == PARTITION_STRATEGY_LIST);
+	Assert(boundinfo && boundinfo->strategy == PARTITION_STRATEGY_LIST);
+
+	/*
+	 * Search each value of new partition "spec" in existing partitions. All
+	 * of them should be in split partition (with Oid splitPartOid).
+	 */
+	foreach(cell, spec->listdatums)
+	{
+		Const	   *val = lfirst_node(Const, cell);
+
+		overlap_location = val->location;
+		if (!val->constisnull)
+		{
+			int			offset;
+			bool		equal;
+
+			offset = partition_list_bsearch(&key->partsupfunc[0],
+											key->partcollation,
+											boundinfo,
+											val->constvalue,
+											&equal);
+			if (offset >= 0 && equal)
+			{
+				with = boundinfo->indexes[offset];
+				if (partdesc->oids[with] != splitPartOid)
+				{
+					overlap = true;
+					break;
+				}
+			}
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("new partition \"%s\" cannot have this value because split partition does not have",
+								relname),
+						 parser_errposition(pstate, overlap_location)));
+		}
+		else if (partition_bound_accepts_nulls(boundinfo))
+		{
+			with = boundinfo->null_index;
+			if (partdesc->oids[with] != splitPartOid)
+			{
+				overlap = true;
+				break;
+			}
+		}
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("new partition \"%s\" cannot have NULL value because split partition does not have",
+							relname),
+					 parser_errposition(pstate, overlap_location)));
+	}
+
+	if (overlap)
+	{
+		Assert(with >= 0);
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("new partition \"%s\" would overlap with another (not split) partition \"%s\"",
+						relname, get_rel_name(partdesc->oids[with])),
+				 parser_errposition(pstate, overlap_location)));
+	}
+}
+
+/*
+ * find_value_in_new_partitions_list
+ *
+ * (function for BY LIST partitioning)
+ *
+ * Function returns true in case any of new partitions contains value "value".
+ *
+ * partsupfunc:		information about comparison function associated with the partition key
+ * partcollation:	partitioning collation
+ * parts:			pointer to array with new partitions descriptions
+ * nparts:			number of new partitions
+ * value:			the value that we are looking for
+ * isnull:			true if the value that we are looking for is NULL
+ */
+static bool
+find_value_in_new_partitions_list(FmgrInfo *partsupfunc,
+								  Oid *partcollation,
+								  SinglePartitionSpec **parts,
+								  int nparts,
+								  Datum value,
+								  bool isnull)
+{
+	ListCell   *valptr;
+	int			i;
+
+	for (i = 0; i < nparts; i++)
+	{
+		SinglePartitionSpec *sps = parts[i];
+
+		foreach(valptr, sps->bound->listdatums)
+		{
+			Const	   *val = lfirst_node(Const, valptr);
+
+			if (isnull && val->constisnull)
+				return true;
+
+			if (!isnull && !val->constisnull)
+			{
+				if (DatumGetInt32(FunctionCall2Coll(&partsupfunc[0],
+													partcollation[0],
+													val->constvalue,
+													value)) == 0)
+					return true;
+			}
+		}
+	}
+	return false;
+}
+
+/*
+ * check_parent_values_in_new_partitions
+ *
+ * (function for BY LIST partitioning)
+ *
+ * Checks that all values of split partition (with Oid partOid) contains in new
+ * partitions.
+ *
+ * parent:	partitioned table
+ * partOid:	split partition Oid
+ * parts:	pointer to array with new partitions descriptions
+ * nparts:	number of new partitions
+ * pstate:	pointer to ParseState struct for determine error position
+ */
+static void
+check_parent_values_in_new_partitions(Relation parent,
+									  Oid partOid,
+									  SinglePartitionSpec **parts,
+									  int nparts,
+									  ParseState *pstate)
+{
+	PartitionKey key = RelationGetPartitionKey(parent);
+	PartitionDesc partdesc = RelationGetPartitionDesc(parent, false);
+	PartitionBoundInfo boundinfo = partdesc->boundinfo;
+	int			i;
+	bool		found = true;
+	bool		searchNull = false;
+	Datum		datum = PointerGetDatum(NULL);
+
+	Assert(key->strategy == PARTITION_STRATEGY_LIST);
+
+	/*
+	 * Special processing for NULL value. Search NULL value if the split
+	 * partition (partOid) contains it.
+	 */
+	if (partition_bound_accepts_nulls(boundinfo) &&
+		partdesc->oids[boundinfo->null_index] == partOid)
+	{
+		if (!find_value_in_new_partitions_list(&key->partsupfunc[0],
+											   key->partcollation, parts, nparts, datum, true))
+		{
+			found = false;
+			searchNull = true;
+		}
+	}
+
+	/*
+	 * Search all values of split partition with partOid in PartitionDesc of
+	 * partitioned table.
+	 */
+	for (i = 0; i < boundinfo->ndatums; i++)
+	{
+		if (partdesc->oids[boundinfo->indexes[i]] == partOid)
+		{
+			/* We found value that split partition contains. */
+			datum = boundinfo->datums[i][0];
+			if (!find_value_in_new_partitions_list(&key->partsupfunc[0],
+												   key->partcollation, parts, nparts, datum, false))
+			{
+				found = false;
+				break;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		Const	   *notFoundVal;
+
+		if (!searchNull)
+
+			/*
+			 * Make Const for getting string representation of not found
+			 * value.
+			 */
+			notFoundVal = makeConst(key->parttypid[0],
+									key->parttypmod[0],
+									key->parttypcoll[0],
+									key->parttyplen[0],
+									datum,
+									false,	/* isnull */
+									key->parttypbyval[0]);
+
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("new partitions do not have value %s but split partition does",
+						searchNull ? "NULL" : get_list_partvalue_string(notFoundVal))));
+	}
+}
+
+/*
+ * check_partitions_for_split
+ *
+ * Checks new partitions for SPLIT PARTITIONS command:
+ * 1. DEFAULT partition should be one.
+ * 2. New partitions should have different names
+ *    (with existing partitions too).
+ * 3. Bounds of new partitions should not overlap with new and existing
+ *    partitions.
+ * 4. In case split partition is DEFAULT partition, one of new partitions
+ *    should be DEFAULT.
+ * 5. In case new partitions or existing partitions contains DEFAULT
+ *    partition, new partitions can have any bounds inside split
+ *    partition bound (can be spaces between partitions bounds).
+ * 6. In case partitioned table does not have DEFAULT partition, DEFAULT
+ *    partition can be defined as one of new partition.
+ * 7. In case new partitions not contains DEFAULT partition and
+ *    partitioned table does not have DEFAULT partition the following
+ *    should be true: sum bounds of new partitions should be equal
+ *    to bound of split partition.
+ *
+ * parent:			partitioned table
+ * splitPartOid:	split partition Oid
+ * splitPartName:	split partition name
+ * list:			list of new partitions
+ * pstate:			pointer to ParseState struct for determine error position
+ */
+void
+check_partitions_for_split(Relation parent,
+						   Oid splitPartOid,
+						   RangeVar *splitPartName,
+						   List *partlist,
+						   ParseState *pstate)
+{
+	PartitionKey key;
+	char		strategy;
+	Oid			defaultPartOid;
+	bool		isSplitPartDefault;
+	bool		existsDefaultPart;
+	ListCell   *listptr;
+	int			default_index = -1;
+	int			i,
+				j;
+	SinglePartitionSpec **new_parts;
+	SinglePartitionSpec *spsPrev = NULL;
+	int			nparts = 0;
+
+	key = RelationGetPartitionKey(parent);
+	strategy = get_partition_strategy(key);
+
+	switch (strategy)
+	{
+		case PARTITION_STRATEGY_LIST:
+		case PARTITION_STRATEGY_RANGE:
+			{
+				/*
+				 * Make array new_parts with new partitions except DEFAULT
+				 * partition.
+				 */
+				new_parts = (SinglePartitionSpec **)
+					palloc0(list_length(partlist) * sizeof(SinglePartitionSpec *));
+				i = 0;
+				foreach(listptr, partlist)
+				{
+					SinglePartitionSpec *sps =
+						(SinglePartitionSpec *) lfirst(listptr);
+
+					if (sps->bound->is_default)
+					{
+						if (default_index >= 0)
+							ereport(ERROR,
+									(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+									 errmsg("DEFAULT partition should be one")),
+									parser_errposition(pstate, sps->name->location));
+						default_index = i;
+					}
+					else
+					{
+						new_parts[nparts++] = sps;
+					}
+					i++;
+				}
+			}
+			break;
+
+		case PARTITION_STRATEGY_HASH:
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("partition of hash-partitioned table cannot be split")));
+			break;
+
+		default:
+			elog(ERROR, "unexpected partition strategy: %d",
+				 (int) key->strategy);
+			break;
+	}
+
+	if (strategy == PARTITION_STRATEGY_RANGE)
+	{
+		PartitionRangeBound **lower_bounds;
+		SinglePartitionSpec **tmp_new_parts;
+
+		/*
+		 * For simplify check for ranges of new partitions need to sort all
+		 * partitions in ascending order of them bounds (we compare upper
+		 * bound only).
+		 */
+		lower_bounds = (PartitionRangeBound **)
+			palloc0(nparts * sizeof(PartitionRangeBound *));
+
+		/* Create array of lower bounds. */
+		for (i = 0; i < nparts; i++)
+		{
+			lower_bounds[i] = make_one_partition_rbound(key, i,
+														new_parts[i]->bound->lowerdatums, true);
+		}
+
+		/* Sort array of lower bounds. */
+		qsort_arg(lower_bounds, nparts, sizeof(PartitionRangeBound *),
+				  qsort_partition_rbound_cmp, (void *) key);
+
+		/* Reorder array of partitions. */
+		tmp_new_parts = new_parts;
+		new_parts = (SinglePartitionSpec **)
+			palloc0(nparts * sizeof(SinglePartitionSpec *));
+		for (i = 0; i < nparts; i++)
+			new_parts[i] = tmp_new_parts[lower_bounds[i]->index];
+
+		pfree(tmp_new_parts);
+		pfree(lower_bounds);
+	}
+
+	defaultPartOid =
+		get_default_oid_from_partdesc(RelationGetPartitionDesc(parent, true));
+
+	/* isSplitPartDefault flag: is split partition a DEFAULT partition? */
+	isSplitPartDefault = (defaultPartOid == splitPartOid);
+
+	if (isSplitPartDefault && default_index < 0)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("one partition in the list should be DEFAULT because split partition is DEFAULT")),
+				parser_errposition(pstate, ((SinglePartitionSpec *) linitial(partlist))->name->location));
+	}
+	else if (!isSplitPartDefault && (default_index >= 0) && OidIsValid(defaultPartOid))
+	{
+		SinglePartitionSpec *spsDef =
+			(SinglePartitionSpec *) list_nth(partlist, default_index);
+
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("new partition cannot be DEFAULT because DEFAULT partition already exists")),
+				parser_errposition(pstate, spsDef->name->location));
+	}
+
+	/* Indicator that partitioned table has (or will have) DEFAULT partition */
+	existsDefaultPart = OidIsValid(defaultPartOid) || (default_index >= 0);
+
+	for (i = 0; i < nparts; i++)
+	{
+		SinglePartitionSpec *sps = new_parts[i];
+
+		if (isSplitPartDefault)
+		{
+			/*
+			 * In case split partition is DEFAULT partition we can use any
+			 * free ranges - as when creating a new partition.
+			 */
+			check_new_partition_bound(sps->name->relname, parent, sps->bound,
+									  pstate);
+		}
+		else
+		{
+			/*
+			 * Checks that bound of current partition is inside bound of split
+			 * partition. For range partitioning: checks that upper bound of
+			 * previous partition is equal to lower bound of current
+			 * partition. For list partitioning: checks that split partition
+			 * contains all values of current partition.
+			 */
+			if (strategy == PARTITION_STRATEGY_RANGE)
+			{
+				bool		first = (i == 0);
+				bool		last = (i == (nparts - 1));
+
+				check_partition_bounds_for_split_range(parent, sps->name->relname, sps->bound,
+													   splitPartOid, splitPartName,
+													   first, last,
+													   existsDefaultPart, pstate);
+			}
+			else
+				check_partition_bounds_for_split_list(parent, sps->name->relname,
+													  sps->bound, splitPartOid, pstate);
+		}
+
+		/* Ranges of new partitions should not overlap. */
+		if (strategy == PARTITION_STRATEGY_RANGE && spsPrev)
+			check_two_partitions_bounds_range(parent, spsPrev->name, spsPrev->bound,
+											  sps->name, sps->bound, existsDefaultPart, pstate);
+
+		spsPrev = sps;
+
+		/* Check: new partitions should have different names. */
+		for (j = i + 1; j < nparts; j++)
+		{
+			SinglePartitionSpec *sps2 = new_parts[j];
+
+			if (equal(sps->name, sps2->name))
+				ereport(ERROR,
+						(errcode(ERRCODE_DUPLICATE_TABLE),
+						 errmsg("name \"%s\" is already used", sps2->name->relname)),
+						parser_errposition(pstate, sps2->name->location));
+		}
+	}
+
+	if (strategy == PARTITION_STRATEGY_LIST)
+	{
+		/* Values of new partitions should not overlap. */
+		check_partitions_not_overlap_list(parent, new_parts, nparts,
+										  pstate);
+
+		/*
+		 * Need to check that all values of split partition contains in new
+		 * partitions. Skip this check if DEFAULT partition exists.
+		 */
+		if (!existsDefaultPart)
+			check_parent_values_in_new_partitions(parent, splitPartOid,
+												  new_parts, nparts, pstate);
+	}
+
+	pfree(new_parts);
+}
+
 /*
  * calculate_partition_bound_for_merge
  *
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 00eda1b34c..4039ee0df7 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -13296,3 +13296,21 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+/*
+ * get_list_partvalue_string
+ *		A C string representation of one list partition value
+ */
+char *
+get_list_partvalue_string(Const *val)
+{
+	deparse_context context;
+	StringInfo	buf = makeStringInfo();
+
+	memset(&context, 0, sizeof(deparse_context));
+	context.buf = buf;
+
+	get_const_expr(val, &context, -1);
+
+	return buf->data;
+}
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index c3ea87fcb9..0d25981253 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -2353,7 +2353,7 @@ psql_completion(const char *text, int start, int end)
 					  "OWNER TO", "SET", "VALIDATE CONSTRAINT",
 					  "REPLICA IDENTITY", "ATTACH PARTITION",
 					  "DETACH PARTITION", "FORCE ROW LEVEL SECURITY",
-					  "MERGE PARTITIONS (",
+					  "SPLIT PARTITION", "MERGE PARTITIONS (",
 					  "OF", "NOT OF");
 	/* ALTER TABLE xxx ADD */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ADD"))
@@ -2610,10 +2610,10 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("FROM (", "IN (", "WITH (");
 
 	/*
-	 * If we have ALTER TABLE <foo> DETACH PARTITION, provide a list of
+	 * If we have ALTER TABLE <foo> DETACH|SPLIT PARTITION, provide a list of
 	 * partitions of <foo>.
 	 */
-	else if (Matches("ALTER", "TABLE", MatchAny, "DETACH", "PARTITION"))
+	else if (Matches("ALTER", "TABLE", MatchAny, "DETACH|SPLIT", "PARTITION"))
 	{
 		set_completion_reference(prev3_wd);
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_partition_of_table);
@@ -2621,6 +2621,10 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("ALTER", "TABLE", MatchAny, "DETACH", "PARTITION", MatchAny))
 		COMPLETE_WITH("CONCURRENTLY", "FINALIZE");
 
+	/* ALTER TABLE <name> SPLIT PARTITION <name> */
+	else if (Matches("ALTER", "TABLE", MatchAny, "SPLIT", "PARTITION", MatchAny))
+		COMPLETE_WITH("INTO ( PARTITION");
+
 	/* ALTER TABLE <name> MERGE PARTITIONS ( */
 	else if (Matches("ALTER", "TABLE", MatchAny, "MERGE", "PARTITIONS", "("))
 	{
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index b6e7957ebb..85a62b538e 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2413,6 +2413,7 @@ typedef enum AlterTableType
 	AT_AttachPartition,			/* ATTACH PARTITION */
 	AT_DetachPartition,			/* DETACH PARTITION */
 	AT_DetachPartitionFinalize, /* DETACH PARTITION FINALIZE */
+	AT_SplitPartition,			/* SPLIT PARTITION */
 	AT_MergePartitions,			/* MERGE PARTITIONS */
 	AT_AddIdentity,				/* ADD IDENTITY */
 	AT_SetIdentity,				/* SET identity column options */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 6b256fcece..bb191b1f46 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -418,6 +418,7 @@ PG_KEYWORD("smallint", SMALLINT, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("snapshot", SNAPSHOT, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("some", SOME, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("source", SOURCE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("split", SPLIT, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("sql", SQL_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("stable", STABLE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("standalone", STANDALONE_P, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/partitioning/partbounds.h b/src/include/partitioning/partbounds.h
index 0329d7bd84..9d2a26705f 100644
--- a/src/include/partitioning/partbounds.h
+++ b/src/include/partitioning/partbounds.h
@@ -143,6 +143,11 @@ extern int	partition_range_datum_bsearch(FmgrInfo *partsupfunc,
 extern int	partition_hash_bsearch(PartitionBoundInfo boundinfo,
 								   int modulus, int remainder);
 
+extern void check_partitions_for_split(Relation parent,
+									   Oid splitPartOid,
+									   RangeVar *splitPartName,
+									   List *partlist,
+									   ParseState *pstate);
 extern void calculate_partition_bound_for_merge(Relation parent,
 												List *partNames,
 												List *partOids,
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 161fb5ef02..6d86080622 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -49,4 +49,6 @@ extern char *get_range_partbound_string(List *bound_datums);
 
 extern char *pg_get_statisticsobjdef_string(Oid statextid);
 
+extern char *get_list_partvalue_string(Const *val);
+
 #endif							/* RULEUTILS_H */
diff --git a/src/test/isolation/expected/partition-split.out b/src/test/isolation/expected/partition-split.out
new file mode 100644
index 0000000000..5d9e8b0925
--- /dev/null
+++ b/src/test/isolation/expected/partition-split.out
@@ -0,0 +1,190 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1b s1splt s2b s2i s1c s2c s2s
+step s1b: BEGIN;
+step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO
+			   (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15),
+			    PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20));
+step s2b: BEGIN;
+step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
+step s1c: COMMIT;
+step s2i: <... completed>
+step s2c: COMMIT;
+step s2s: SELECT * FROM tpart;
+ i|t     
+--+------
+ 5|text05
+ 1|text01
+15|text15
+25|text25
+35|text35
+(5 rows)
+
+
+starting permutation: s1b s1splt s2brr s2i s1c s2c s2s
+step s1b: BEGIN;
+step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO
+			   (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15),
+			    PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20));
+step s2brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
+step s1c: COMMIT;
+step s2i: <... completed>
+step s2c: COMMIT;
+step s2s: SELECT * FROM tpart;
+ i|t     
+--+------
+ 5|text05
+ 1|text01
+15|text15
+25|text25
+35|text35
+(5 rows)
+
+
+starting permutation: s1b s1splt s2bs s2i s1c s2c s2s
+step s1b: BEGIN;
+step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO
+			   (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15),
+			    PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20));
+step s2bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
+step s1c: COMMIT;
+step s2i: <... completed>
+step s2c: COMMIT;
+step s2s: SELECT * FROM tpart;
+ i|t     
+--+------
+ 5|text05
+ 1|text01
+15|text15
+25|text25
+35|text35
+(5 rows)
+
+
+starting permutation: s1brr s1splt s2b s2i s1c s2c s2s
+step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO
+			   (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15),
+			    PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20));
+step s2b: BEGIN;
+step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
+step s1c: COMMIT;
+step s2i: <... completed>
+step s2c: COMMIT;
+step s2s: SELECT * FROM tpart;
+ i|t     
+--+------
+ 5|text05
+ 1|text01
+15|text15
+25|text25
+35|text35
+(5 rows)
+
+
+starting permutation: s1brr s1splt s2brr s2i s1c s2c s2s
+step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO
+			   (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15),
+			    PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20));
+step s2brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
+step s1c: COMMIT;
+step s2i: <... completed>
+step s2c: COMMIT;
+step s2s: SELECT * FROM tpart;
+ i|t     
+--+------
+ 5|text05
+ 1|text01
+15|text15
+25|text25
+35|text35
+(5 rows)
+
+
+starting permutation: s1brr s1splt s2bs s2i s1c s2c s2s
+step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO
+			   (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15),
+			    PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20));
+step s2bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
+step s1c: COMMIT;
+step s2i: <... completed>
+step s2c: COMMIT;
+step s2s: SELECT * FROM tpart;
+ i|t     
+--+------
+ 5|text05
+ 1|text01
+15|text15
+25|text25
+35|text35
+(5 rows)
+
+
+starting permutation: s1bs s1splt s2b s2i s1c s2c s2s
+step s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO
+			   (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15),
+			    PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20));
+step s2b: BEGIN;
+step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
+step s1c: COMMIT;
+step s2i: <... completed>
+step s2c: COMMIT;
+step s2s: SELECT * FROM tpart;
+ i|t     
+--+------
+ 5|text05
+ 1|text01
+15|text15
+25|text25
+35|text35
+(5 rows)
+
+
+starting permutation: s1bs s1splt s2brr s2i s1c s2c s2s
+step s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO
+			   (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15),
+			    PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20));
+step s2brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
+step s1c: COMMIT;
+step s2i: <... completed>
+step s2c: COMMIT;
+step s2s: SELECT * FROM tpart;
+ i|t     
+--+------
+ 5|text05
+ 1|text01
+15|text15
+25|text25
+35|text35
+(5 rows)
+
+
+starting permutation: s1bs s1splt s2bs s2i s1c s2c s2s
+step s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO
+			   (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15),
+			    PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20));
+step s2bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
+step s1c: COMMIT;
+step s2i: <... completed>
+step s2c: COMMIT;
+step s2s: SELECT * FROM tpart;
+ i|t     
+--+------
+ 5|text05
+ 1|text01
+15|text15
+25|text25
+35|text35
+(5 rows)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index c1499338d6..6da98cffac 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -107,6 +107,7 @@ test: partition-key-update-2
 test: partition-key-update-3
 test: partition-key-update-4
 test: partition-merge
+test: partition-split
 test: plpgsql-toast
 test: cluster-conflict
 test: cluster-conflict-partition
diff --git a/src/test/isolation/specs/partition-split.spec b/src/test/isolation/specs/partition-split.spec
new file mode 100644
index 0000000000..087239a4a1
--- /dev/null
+++ b/src/test/isolation/specs/partition-split.spec
@@ -0,0 +1,54 @@
+# Verify that SPLIT operation locks DML operations with partitioned table
+
+setup
+{
+  DROP TABLE IF EXISTS tpart;
+  CREATE TABLE tpart(i int, t text) partition by range(i);
+  CREATE TABLE tpart_00_10 PARTITION OF tpart FOR VALUES FROM (0) TO (10);
+  CREATE TABLE tpart_10_20 PARTITION OF tpart FOR VALUES FROM (10) TO (20);
+  CREATE TABLE tpart_20_30 PARTITION OF tpart FOR VALUES FROM (20) TO (30);
+  CREATE TABLE tpart_default PARTITION OF tpart DEFAULT;
+  INSERT INTO tpart VALUES (5, 'text05');
+  INSERT INTO tpart VALUES (15, 'text15');
+  INSERT INTO tpart VALUES (25, 'text25');
+  INSERT INTO tpart VALUES (35, 'text35');
+}
+
+teardown
+{
+  DROP TABLE tpart;
+}
+
+session s1
+step s1b	{ BEGIN; }
+step s1brr	{ BEGIN ISOLATION LEVEL REPEATABLE READ; }
+step s1bs	{ BEGIN ISOLATION LEVEL SERIALIZABLE; }
+step s1splt	{ ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO
+			   (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15),
+			    PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20)); }
+step s1c	{ COMMIT; }
+
+
+session s2
+step s2b	{ BEGIN; }
+step s2brr	{ BEGIN ISOLATION LEVEL REPEATABLE READ; }
+step s2bs	{ BEGIN ISOLATION LEVEL SERIALIZABLE; }
+step s2i	{ INSERT INTO tpart VALUES (1, 'text01'); }
+step s2c	{ COMMIT; }
+step s2s	{ SELECT * FROM tpart; }
+
+
+# s1 starts SPLIT PARTITION then s2 trying to insert row and
+# waits until s1 finished SPLIT operation.
+
+permutation s1b s1splt s2b s2i s1c s2c s2s
+permutation s1b s1splt s2brr s2i s1c s2c s2s
+permutation s1b s1splt s2bs s2i s1c s2c s2s
+
+permutation s1brr s1splt s2b s2i s1c s2c s2s
+permutation s1brr s1splt s2brr s2i s1c s2c s2s
+permutation s1brr s1splt s2bs s2i s1c s2c s2s
+
+permutation s1bs s1splt s2b s2i s1c s2c s2s
+permutation s1bs s1splt s2brr s2i s1c s2c s2s
+permutation s1bs s1splt s2bs s2i s1c s2c s2s
diff --git a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
index cc341afdf4..67ff2b6367 100644
--- a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
+++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
@@ -300,6 +300,9 @@ get_altertable_subcmdinfo(PG_FUNCTION_ARGS)
 			case AT_DetachPartitionFinalize:
 				strtype = "DETACH PARTITION ... FINALIZE";
 				break;
+			case AT_SplitPartition:
+				strtype = "SPLIT PARTITION";
+				break;
 			case AT_MergePartitions:
 				strtype = "MERGE PARTITIONS";
 				break;
diff --git a/src/test/regress/expected/partition_split.out b/src/test/regress/expected/partition_split.out
new file mode 100644
index 0000000000..dc9a5130cc
--- /dev/null
+++ b/src/test/regress/expected/partition_split.out
@@ -0,0 +1,1589 @@
+--
+-- PARTITION_SPLIT
+-- Tests for "ALTER TABLE ... SPLIT PARTITION ..." command
+--
+CREATE SCHEMA partition_split_schema;
+CREATE SCHEMA partition_split_schema2;
+SET search_path = partition_split_schema, public;
+--
+-- BY RANGE partitioning
+--
+--
+-- Test for error codes
+--
+CREATE TABLE sales_range (salesperson_id int, salesperson_name varchar(30), sales_amount int, sales_date date) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01');
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+-- ERROR:  relation "sales_xxx" does not exist
+ALTER TABLE sales_range SPLIT PARTITION sales_xxx INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+ERROR:  relation "sales_xxx" does not exist
+-- ERROR:  relation "sales_jan2022" already exists
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_jan2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+ERROR:  relation "sales_jan2022" already exists
+-- ERROR:  invalid bound specification for a range partition
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_jan2022 FOR VALUES IN ('2022-05-01', '2022-06-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+ERROR:  invalid bound specification for a range partition
+LINE 2:   (PARTITION sales_jan2022 FOR VALUES IN ('2022-05-01', '202...
+                                              ^
+-- ERROR:  empty range bound specified for partition "sales_mar2022"
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-02-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+ERROR:  empty range bound specified for partition "sales_mar2022"
+LINE 3:    PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO...
+                                                    ^
+DETAIL:  Specified lower bound ('03-01-2022') is greater than or equal to upper bound ('02-01-2022').
+--ERROR:  list of split partitions should contain at least two items
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-10-01'));
+ERROR:  list of new partitions should contain at least two items
+-- ERROR:  lower bound of partition "sales_feb2022" is less than lower bound of split partition
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-01-01') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+ERROR:  lower bound of partition "sales_feb2022" is less than lower bound of split partition
+LINE 2:   (PARTITION sales_feb2022 FOR VALUES FROM ('2022-01-01') TO...
+                                                    ^
+-- ERROR:  name "sales_feb_mar_apr2022" is already used
+-- (We can create partition with the same name as split partition, but can't create two partitions with the same name)
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb_mar_apr2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_feb_mar_apr2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+ERROR:  name "sales_feb_mar_apr2022" is already used
+LINE 3:    PARTITION sales_feb_mar_apr2022 FOR VALUES FROM ('2022-03...
+                     ^
+-- ERROR:  name "sales_feb2022" is already used
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_feb2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+ERROR:  name "sales_feb2022" is already used
+LINE 3:    PARTITION sales_feb2022 FOR VALUES FROM ('2022-03-01') TO...
+                     ^
+-- ERROR:  "sales_feb_mar_apr2022" is not a partitioned table
+ALTER TABLE sales_feb_mar_apr2022 SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_jan2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_feb2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+ERROR:  "sales_feb_mar_apr2022" is not a partitioned table
+-- ERROR:  upper bound of partition "sales_apr2022" is greater than upper bound of split partition
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-06-01'));
+ERROR:  upper bound of partition "sales_apr2022" is greater than upper bound of split partition
+LINE 4: ... sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-06-0...
+                                                             ^
+-- ERROR:  lower bound of partition "sales_mar2022" conflicts with upper bound of previous partition "sales_feb2022"
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-02-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+ERROR:  lower bound of partition "sales_mar2022" conflicts with upper bound of previous partition "sales_feb2022"
+LINE 3:    PARTITION sales_mar2022 FOR VALUES FROM ('2022-02-01') TO...
+                                                    ^
+-- Tests for spaces between partitions, them should be executed without DEFAULT partition
+ALTER TABLE sales_range DETACH PARTITION sales_others;
+-- ERROR:  lower bound of partition "sales_feb2022" is not equal to lower bound of split partition
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-02') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+ERROR:  lower bound of partition "sales_feb2022" is not equal to lower bound of split partition
+LINE 2:   (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-02') TO...
+                                                    ^
+-- Check the source partition not in the search path
+SET search_path = partition_split_schema2, public;
+ALTER TABLE partition_split_schema.sales_range
+SPLIT PARTITION partition_split_schema.sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+SET search_path = partition_split_schema, public;
+\d+ sales_range
+                              Partitioned table "partition_split_schema.sales_range"
+      Column      |         Type          | Collation | Nullable | Default | Storage  | Stats target | Description 
+------------------+-----------------------+-----------+----------+---------+----------+--------------+-------------
+ salesperson_id   | integer               |           |          |         | plain    |              | 
+ salesperson_name | character varying(30) |           |          |         | extended |              | 
+ sales_amount     | integer               |           |          |         | plain    |              | 
+ sales_date       | date                  |           |          |         | plain    |              | 
+Partition key: RANGE (sales_date)
+Partitions: partition_split_schema2.sales_apr2022 FOR VALUES FROM ('04-01-2022') TO ('05-01-2022'),
+            partition_split_schema2.sales_feb2022 FOR VALUES FROM ('02-01-2022') TO ('03-01-2022'),
+            partition_split_schema2.sales_mar2022 FOR VALUES FROM ('03-01-2022') TO ('04-01-2022'),
+            sales_jan2022 FOR VALUES FROM ('01-01-2022') TO ('02-01-2022')
+
+DROP TABLE sales_range;
+DROP TABLE sales_others;
+--
+-- Add rows into partitioned table then split partition
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01');
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-10');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
+INSERT INTO sales_range VALUES (4,  'Ivanov',   750,  '2022-04-13');
+INSERT INTO sales_range VALUES (5,  'Deev',     250,  '2022-04-07');
+INSERT INTO sales_range VALUES (6,  'Poirot',   150,  '2022-02-11');
+INSERT INTO sales_range VALUES (7,  'Li',       175,  '2022-03-08');
+INSERT INTO sales_range VALUES (8,  'Ericsson', 185,  '2022-02-23');
+INSERT INTO sales_range VALUES (9,  'Muller',   250,  '2022-03-11');
+INSERT INTO sales_range VALUES (10, 'Halder',   350,  '2022-01-28');
+INSERT INTO sales_range VALUES (11, 'Trump',    380,  '2022-04-06');
+INSERT INTO sales_range VALUES (12, 'Plato',    350,  '2022-03-19');
+INSERT INTO sales_range VALUES (13, 'Gandi',    377,  '2022-01-09');
+INSERT INTO sales_range VALUES (14, 'Smith',    510,  '2022-05-04');
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+SELECT * FROM sales_range;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              1 | May              |         1000 | 01-31-2022
+             10 | Halder           |          350 | 01-28-2022
+             13 | Gandi            |          377 | 01-09-2022
+              2 | Smirnoff         |          500 | 02-10-2022
+              6 | Poirot           |          150 | 02-11-2022
+              8 | Ericsson         |          185 | 02-23-2022
+              7 | Li               |          175 | 03-08-2022
+              9 | Muller           |          250 | 03-11-2022
+             12 | Plato            |          350 | 03-19-2022
+              3 | Ford             |         2000 | 04-30-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+             14 | Smith            |          510 | 05-04-2022
+(14 rows)
+
+SELECT * FROM sales_jan2022;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              1 | May              |         1000 | 01-31-2022
+             10 | Halder           |          350 | 01-28-2022
+             13 | Gandi            |          377 | 01-09-2022
+(3 rows)
+
+SELECT * FROM sales_feb2022;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              2 | Smirnoff         |          500 | 02-10-2022
+              6 | Poirot           |          150 | 02-11-2022
+              8 | Ericsson         |          185 | 02-23-2022
+(3 rows)
+
+SELECT * FROM sales_mar2022;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              7 | Li               |          175 | 03-08-2022
+              9 | Muller           |          250 | 03-11-2022
+             12 | Plato            |          350 | 03-19-2022
+(3 rows)
+
+SELECT * FROM sales_apr2022;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              3 | Ford             |         2000 | 04-30-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+(4 rows)
+
+SELECT * FROM sales_others;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+             14 | Smith            |          510 | 05-04-2022
+(1 row)
+
+DROP TABLE sales_range CASCADE;
+--
+-- Add split partition, then add rows into partitioned table
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01');
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+-- Split partition, also check schema qualification of new partitions
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION partition_split_schema.sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION partition_split_schema2.sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+\d+ sales_range
+                              Partitioned table "partition_split_schema.sales_range"
+      Column      |         Type          | Collation | Nullable | Default | Storage  | Stats target | Description 
+------------------+-----------------------+-----------+----------+---------+----------+--------------+-------------
+ salesperson_id   | integer               |           |          |         | plain    |              | 
+ salesperson_name | character varying(30) |           |          |         | extended |              | 
+ sales_amount     | integer               |           |          |         | plain    |              | 
+ sales_date       | date                  |           |          |         | plain    |              | 
+Partition key: RANGE (sales_date)
+Partitions: partition_split_schema2.sales_mar2022 FOR VALUES FROM ('03-01-2022') TO ('04-01-2022'),
+            sales_apr2022 FOR VALUES FROM ('04-01-2022') TO ('05-01-2022'),
+            sales_feb2022 FOR VALUES FROM ('02-01-2022') TO ('03-01-2022'),
+            sales_jan2022 FOR VALUES FROM ('01-01-2022') TO ('02-01-2022'),
+            sales_others DEFAULT
+
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-10');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
+INSERT INTO sales_range VALUES (4,  'Ivanov',   750,  '2022-04-13');
+INSERT INTO sales_range VALUES (5,  'Deev',     250,  '2022-04-07');
+INSERT INTO sales_range VALUES (6,  'Poirot',   150,  '2022-02-11');
+INSERT INTO sales_range VALUES (7,  'Li',       175,  '2022-03-08');
+INSERT INTO sales_range VALUES (8,  'Ericsson', 185,  '2022-02-23');
+INSERT INTO sales_range VALUES (9,  'Muller',   250,  '2022-03-11');
+INSERT INTO sales_range VALUES (10, 'Halder',   350,  '2022-01-28');
+INSERT INTO sales_range VALUES (11, 'Trump',    380,  '2022-04-06');
+INSERT INTO sales_range VALUES (12, 'Plato',    350,  '2022-03-19');
+INSERT INTO sales_range VALUES (13, 'Gandi',    377,  '2022-01-09');
+INSERT INTO sales_range VALUES (14, 'Smith',    510,  '2022-05-04');
+SELECT * FROM sales_range;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              1 | May              |         1000 | 01-31-2022
+             10 | Halder           |          350 | 01-28-2022
+             13 | Gandi            |          377 | 01-09-2022
+              2 | Smirnoff         |          500 | 02-10-2022
+              6 | Poirot           |          150 | 02-11-2022
+              8 | Ericsson         |          185 | 02-23-2022
+              7 | Li               |          175 | 03-08-2022
+              9 | Muller           |          250 | 03-11-2022
+             12 | Plato            |          350 | 03-19-2022
+              3 | Ford             |         2000 | 04-30-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+             14 | Smith            |          510 | 05-04-2022
+(14 rows)
+
+SELECT * FROM sales_jan2022;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              1 | May              |         1000 | 01-31-2022
+             10 | Halder           |          350 | 01-28-2022
+             13 | Gandi            |          377 | 01-09-2022
+(3 rows)
+
+SELECT * FROM sales_feb2022;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              2 | Smirnoff         |          500 | 02-10-2022
+              6 | Poirot           |          150 | 02-11-2022
+              8 | Ericsson         |          185 | 02-23-2022
+(3 rows)
+
+SELECT * FROM partition_split_schema2.sales_mar2022;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              7 | Li               |          175 | 03-08-2022
+              9 | Muller           |          250 | 03-11-2022
+             12 | Plato            |          350 | 03-19-2022
+(3 rows)
+
+SELECT * FROM sales_apr2022;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              3 | Ford             |         2000 | 04-30-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+(4 rows)
+
+SELECT * FROM sales_others;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+             14 | Smith            |          510 | 05-04-2022
+(1 row)
+
+DROP TABLE sales_range CASCADE;
+--
+-- Test for:
+--   * composite partition key;
+--   * GENERATED column;
+--   * column with DEFAULT value.
+--
+CREATE TABLE sales_date (salesperson_name VARCHAR(30), sales_year INT, sales_month INT, sales_day INT,
+  sales_date VARCHAR(10) GENERATED ALWAYS AS
+    (LPAD(sales_year::text, 4, '0') || '.' || LPAD(sales_month::text, 2, '0') || '.' || LPAD(sales_day::text, 2, '0')) STORED,
+  sales_department VARCHAR(30) DEFAULT 'Sales department')
+  PARTITION BY RANGE (sales_year, sales_month, sales_day);
+CREATE TABLE sales_dec2022 PARTITION OF sales_date FOR VALUES FROM (2021, 12, 1) TO (2022, 1, 1);
+CREATE TABLE sales_jan_feb2022 PARTITION OF sales_date FOR VALUES FROM (2022, 1, 1) TO (2022, 3, 1);
+CREATE TABLE sales_other PARTITION OF sales_date FOR VALUES FROM (2022, 3, 1) TO (MAXVALUE, MAXVALUE, MAXVALUE);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2021, 12, 7);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2021, 12, 8);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 1, 1);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 2, 4);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 1, 2);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 2, 1);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 3, 3);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 3, 4);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 5, 1);
+SELECT * FROM sales_date;
+ salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department 
+------------------+------------+-------------+-----------+------------+------------------
+ Manager1         |       2021 |          12 |         7 | 2021.12.07 | Sales department
+ Manager2         |       2021 |          12 |         8 | 2021.12.08 | Sales department
+ Manager3         |       2022 |           1 |         1 | 2022.01.01 | Sales department
+ Manager1         |       2022 |           2 |         4 | 2022.02.04 | Sales department
+ Manager2         |       2022 |           1 |         2 | 2022.01.02 | Sales department
+ Manager3         |       2022 |           2 |         1 | 2022.02.01 | Sales department
+ Manager1         |       2022 |           3 |         3 | 2022.03.03 | Sales department
+ Manager2         |       2022 |           3 |         4 | 2022.03.04 | Sales department
+ Manager3         |       2022 |           5 |         1 | 2022.05.01 | Sales department
+(9 rows)
+
+SELECT * FROM sales_dec2022;
+ salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department 
+------------------+------------+-------------+-----------+------------+------------------
+ Manager1         |       2021 |          12 |         7 | 2021.12.07 | Sales department
+ Manager2         |       2021 |          12 |         8 | 2021.12.08 | Sales department
+(2 rows)
+
+SELECT * FROM sales_jan_feb2022;
+ salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department 
+------------------+------------+-------------+-----------+------------+------------------
+ Manager3         |       2022 |           1 |         1 | 2022.01.01 | Sales department
+ Manager1         |       2022 |           2 |         4 | 2022.02.04 | Sales department
+ Manager2         |       2022 |           1 |         2 | 2022.01.02 | Sales department
+ Manager3         |       2022 |           2 |         1 | 2022.02.01 | Sales department
+(4 rows)
+
+SELECT * FROM sales_other;
+ salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department 
+------------------+------------+-------------+-----------+------------+------------------
+ Manager1         |       2022 |           3 |         3 | 2022.03.03 | Sales department
+ Manager2         |       2022 |           3 |         4 | 2022.03.04 | Sales department
+ Manager3         |       2022 |           5 |         1 | 2022.05.01 | Sales department
+(3 rows)
+
+ALTER TABLE sales_date SPLIT PARTITION sales_jan_feb2022 INTO
+  (PARTITION sales_jan2022 FOR VALUES FROM (2022, 1, 1) TO (2022, 2, 1),
+   PARTITION sales_feb2022 FOR VALUES FROM (2022, 2, 1) TO (2022, 3, 1));
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 1, 10);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 2, 10);
+SELECT * FROM sales_date;
+ salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department 
+------------------+------------+-------------+-----------+------------+------------------
+ Manager1         |       2021 |          12 |         7 | 2021.12.07 | Sales department
+ Manager2         |       2021 |          12 |         8 | 2021.12.08 | Sales department
+ Manager3         |       2022 |           1 |         1 | 2022.01.01 | Sales department
+ Manager2         |       2022 |           1 |         2 | 2022.01.02 | Sales department
+ Manager1         |       2022 |           1 |        10 | 2022.01.10 | Sales department
+ Manager1         |       2022 |           2 |         4 | 2022.02.04 | Sales department
+ Manager3         |       2022 |           2 |         1 | 2022.02.01 | Sales department
+ Manager2         |       2022 |           2 |        10 | 2022.02.10 | Sales department
+ Manager1         |       2022 |           3 |         3 | 2022.03.03 | Sales department
+ Manager2         |       2022 |           3 |         4 | 2022.03.04 | Sales department
+ Manager3         |       2022 |           5 |         1 | 2022.05.01 | Sales department
+(11 rows)
+
+SELECT * FROM sales_dec2022;
+ salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department 
+------------------+------------+-------------+-----------+------------+------------------
+ Manager1         |       2021 |          12 |         7 | 2021.12.07 | Sales department
+ Manager2         |       2021 |          12 |         8 | 2021.12.08 | Sales department
+(2 rows)
+
+SELECT * FROM sales_jan2022;
+ salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department 
+------------------+------------+-------------+-----------+------------+------------------
+ Manager3         |       2022 |           1 |         1 | 2022.01.01 | Sales department
+ Manager2         |       2022 |           1 |         2 | 2022.01.02 | Sales department
+ Manager1         |       2022 |           1 |        10 | 2022.01.10 | Sales department
+(3 rows)
+
+SELECT * FROM sales_feb2022;
+ salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department 
+------------------+------------+-------------+-----------+------------+------------------
+ Manager1         |       2022 |           2 |         4 | 2022.02.04 | Sales department
+ Manager3         |       2022 |           2 |         1 | 2022.02.01 | Sales department
+ Manager2         |       2022 |           2 |        10 | 2022.02.10 | Sales department
+(3 rows)
+
+SELECT * FROM sales_other;
+ salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department 
+------------------+------------+-------------+-----------+------------+------------------
+ Manager1         |       2022 |           3 |         3 | 2022.03.03 | Sales department
+ Manager2         |       2022 |           3 |         4 | 2022.03.04 | Sales department
+ Manager3         |       2022 |           5 |         1 | 2022.05.01 | Sales department
+(3 rows)
+
+--ERROR:  relation "sales_jan_feb2022" does not exist
+SELECT * FROM sales_jan_feb2022;
+ERROR:  relation "sales_jan_feb2022" does not exist
+LINE 1: SELECT * FROM sales_jan_feb2022;
+                      ^
+DROP TABLE sales_date CASCADE;
+--
+-- Test: split DEFAULT partition; use an index on partition key; check index after split
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree (sales_date);
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-10');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
+INSERT INTO sales_range VALUES (4,  'Ivanov',   750,  '2022-04-13');
+INSERT INTO sales_range VALUES (5,  'Deev',     250,  '2022-04-07');
+INSERT INTO sales_range VALUES (6,  'Poirot',   150,  '2022-02-11');
+INSERT INTO sales_range VALUES (7,  'Li',       175,  '2022-03-08');
+INSERT INTO sales_range VALUES (8,  'Ericsson', 185,  '2022-02-23');
+INSERT INTO sales_range VALUES (9,  'Muller',   250,  '2022-03-11');
+INSERT INTO sales_range VALUES (10, 'Halder',   350,  '2022-01-28');
+INSERT INTO sales_range VALUES (11, 'Trump',    380,  '2022-04-06');
+INSERT INTO sales_range VALUES (12, 'Plato',    350,  '2022-03-19');
+INSERT INTO sales_range VALUES (13, 'Gandi',    377,  '2022-01-09');
+INSERT INTO sales_range VALUES (14, 'Smith',    510,  '2022-05-04');
+SELECT * FROM sales_others;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              2 | Smirnoff         |          500 | 02-10-2022
+              3 | Ford             |         2000 | 04-30-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              5 | Deev             |          250 | 04-07-2022
+              6 | Poirot           |          150 | 02-11-2022
+              7 | Li               |          175 | 03-08-2022
+              8 | Ericsson         |          185 | 02-23-2022
+              9 | Muller           |          250 | 03-11-2022
+             11 | Trump            |          380 | 04-06-2022
+             12 | Plato            |          350 | 03-19-2022
+             14 | Smith            |          510 | 05-04-2022
+(11 rows)
+
+SELECT * FROM pg_indexes WHERE tablename = 'sales_others' and schemaname = 'partition_split_schema' ORDER BY indexname;
+       schemaname       |  tablename   |          indexname          | tablespace |                                                 indexdef                                                 
+------------------------+--------------+-----------------------------+------------+----------------------------------------------------------------------------------------------------------
+ partition_split_schema | sales_others | sales_others_sales_date_idx |            | CREATE INDEX sales_others_sales_date_idx ON partition_split_schema.sales_others USING btree (sales_date)
+(1 row)
+
+ALTER TABLE sales_range SPLIT PARTITION sales_others INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'),
+   PARTITION sales_others DEFAULT);
+-- Use indexscan for testing indexes
+SET enable_indexscan = ON;
+SET enable_seqscan = OFF;
+SELECT * FROM sales_feb2022 where sales_date > '2022-01-01';
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              2 | Smirnoff         |          500 | 02-10-2022
+              6 | Poirot           |          150 | 02-11-2022
+              8 | Ericsson         |          185 | 02-23-2022
+(3 rows)
+
+SELECT * FROM sales_mar2022 where sales_date > '2022-01-01';
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              7 | Li               |          175 | 03-08-2022
+              9 | Muller           |          250 | 03-11-2022
+             12 | Plato            |          350 | 03-19-2022
+(3 rows)
+
+SELECT * FROM sales_apr2022 where sales_date > '2022-01-01';
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+             11 | Trump            |          380 | 04-06-2022
+              5 | Deev             |          250 | 04-07-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              3 | Ford             |         2000 | 04-30-2022
+(4 rows)
+
+SELECT * FROM sales_others where sales_date > '2022-01-01';
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+             14 | Smith            |          510 | 05-04-2022
+(1 row)
+
+SET enable_indexscan = ON;
+SET enable_seqscan = ON;
+SELECT * FROM pg_indexes WHERE tablename = 'sales_feb2022' and schemaname = 'partition_split_schema' ORDER BY indexname;
+       schemaname       |   tablename   |          indexname           | tablespace |                                                  indexdef                                                  
+------------------------+---------------+------------------------------+------------+------------------------------------------------------------------------------------------------------------
+ partition_split_schema | sales_feb2022 | sales_feb2022_sales_date_idx |            | CREATE INDEX sales_feb2022_sales_date_idx ON partition_split_schema.sales_feb2022 USING btree (sales_date)
+(1 row)
+
+SELECT * FROM pg_indexes WHERE tablename = 'sales_mar2022' and schemaname = 'partition_split_schema' ORDER BY indexname;
+       schemaname       |   tablename   |          indexname           | tablespace |                                                  indexdef                                                  
+------------------------+---------------+------------------------------+------------+------------------------------------------------------------------------------------------------------------
+ partition_split_schema | sales_mar2022 | sales_mar2022_sales_date_idx |            | CREATE INDEX sales_mar2022_sales_date_idx ON partition_split_schema.sales_mar2022 USING btree (sales_date)
+(1 row)
+
+SELECT * FROM pg_indexes WHERE tablename = 'sales_apr2022' and schemaname = 'partition_split_schema' ORDER BY indexname;
+       schemaname       |   tablename   |          indexname           | tablespace |                                                  indexdef                                                  
+------------------------+---------------+------------------------------+------------+------------------------------------------------------------------------------------------------------------
+ partition_split_schema | sales_apr2022 | sales_apr2022_sales_date_idx |            | CREATE INDEX sales_apr2022_sales_date_idx ON partition_split_schema.sales_apr2022 USING btree (sales_date)
+(1 row)
+
+SELECT * FROM pg_indexes WHERE tablename = 'sales_others' and schemaname = 'partition_split_schema' ORDER BY indexname;
+       schemaname       |  tablename   |          indexname           | tablespace |                                                 indexdef                                                  
+------------------------+--------------+------------------------------+------------+-----------------------------------------------------------------------------------------------------------
+ partition_split_schema | sales_others | sales_others_sales_date_idx1 |            | CREATE INDEX sales_others_sales_date_idx1 ON partition_split_schema.sales_others USING btree (sales_date)
+(1 row)
+
+DROP TABLE sales_range CASCADE;
+--
+-- Test: some cases for splitting DEFAULT partition (different bounds)
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date INT) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+-- sales_error intersects with sales_dec2022 (lower bound)
+-- ERROR:  lower bound of partition "sales_error" conflicts with upper bound of previous partition "sales_dec2022"
+ALTER TABLE sales_range SPLIT PARTITION sales_others INTO
+  (PARTITION sales_dec2022 FOR VALUES FROM (20211201) TO (20220101),
+   PARTITION sales_error FOR VALUES FROM (20211230) TO (20220201),
+   PARTITION sales_feb2022 FOR VALUES FROM (20220201) TO (20220301),
+   PARTITION sales_others DEFAULT);
+ERROR:  lower bound of partition "sales_error" conflicts with upper bound of previous partition "sales_dec2022"
+LINE 3:    PARTITION sales_error FOR VALUES FROM (20211230) TO (2022...
+                                                  ^
+-- sales_error intersects with sales_feb2022 (upper bound)
+-- ERROR:  lower bound of partition "sales_feb2022" conflicts with upper bound of previous partition "sales_error"
+ALTER TABLE sales_range SPLIT PARTITION sales_others INTO
+  (PARTITION sales_dec2022 FOR VALUES FROM (20211201) TO (20220101),
+   PARTITION sales_error FOR VALUES FROM (20220101) TO (20220202),
+   PARTITION sales_feb2022 FOR VALUES FROM (20220201) TO (20220301),
+   PARTITION sales_others DEFAULT);
+ERROR:  lower bound of partition "sales_feb2022" conflicts with upper bound of previous partition "sales_error"
+LINE 4:    PARTITION sales_feb2022 FOR VALUES FROM (20220201) TO (20...
+                                                    ^
+-- sales_error intersects with sales_dec2022 (inside bound)
+-- ERROR:  lower bound of partition "sales_error" conflicts with upper bound of previous partition "sales_dec2022"
+ALTER TABLE sales_range SPLIT PARTITION sales_others INTO
+  (PARTITION sales_dec2022 FOR VALUES FROM (20211201) TO (20220101),
+   PARTITION sales_error FOR VALUES FROM (20211210) TO (20211220),
+   PARTITION sales_feb2022 FOR VALUES FROM (20220201) TO (20220301),
+   PARTITION sales_others DEFAULT);
+ERROR:  lower bound of partition "sales_error" conflicts with upper bound of previous partition "sales_dec2022"
+LINE 3:    PARTITION sales_error FOR VALUES FROM (20211210) TO (2021...
+                                                  ^
+-- sales_error intersects with sales_dec2022 (exactly the same bounds)
+-- ERROR:  lower bound of partition "sales_error" conflicts with upper bound of previous partition "sales_dec2022"
+ALTER TABLE sales_range SPLIT PARTITION sales_others INTO
+  (PARTITION sales_dec2022 FOR VALUES FROM (20211201) TO (20220101),
+   PARTITION sales_error FOR VALUES FROM (20211201) TO (20220101),
+   PARTITION sales_feb2022 FOR VALUES FROM (20220201) TO (20220301),
+   PARTITION sales_others DEFAULT);
+ERROR:  lower bound of partition "sales_error" conflicts with upper bound of previous partition "sales_dec2022"
+LINE 3:    PARTITION sales_error FOR VALUES FROM (20211201) TO (2022...
+                                                  ^
+-- ERROR:  all partitions in the list should be DEFAULT because split partition is DEFAULT
+ALTER TABLE sales_range SPLIT PARTITION sales_others INTO
+  (PARTITION sales_dec2022 FOR VALUES FROM (20211201) TO (20220101),
+   PARTITION sales_jan2022 FOR VALUES FROM (20220101) TO (20220201),
+   PARTITION sales_feb2022 FOR VALUES FROM (20220201) TO (20220301));
+ERROR:  one partition in the list should be DEFAULT because split partition is DEFAULT
+LINE 2:   (PARTITION sales_dec2022 FOR VALUES FROM (20211201) TO (20...
+                     ^
+-- no error: bounds of sales_noerror are between sales_dec2022 and sales_feb2022
+ALTER TABLE sales_range SPLIT PARTITION sales_others INTO
+  (PARTITION sales_dec2022 FOR VALUES FROM (20211201) TO (20220101),
+   PARTITION sales_noerror FOR VALUES FROM (20220110) TO (20220120),
+   PARTITION sales_feb2022 FOR VALUES FROM (20220201) TO (20220301),
+   PARTITION sales_others DEFAULT);
+DROP TABLE sales_range;
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date INT) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+-- no error: bounds of sales_noerror are equal to lower and upper bounds of sales_dec2022 and sales_feb2022
+ALTER TABLE sales_range SPLIT PARTITION sales_others INTO
+  (PARTITION sales_dec2022 FOR VALUES FROM (20211201) TO (20220101),
+   PARTITION sales_noerror FOR VALUES FROM (20210101) TO (20210201),
+   PARTITION sales_feb2022 FOR VALUES FROM (20220201) TO (20220301),
+   PARTITION sales_others DEFAULT);
+DROP TABLE sales_range;
+--
+-- Test: split partition with CHECK and FOREIGN KEY CONSTRAINTs on partitioned table
+--
+CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30));
+INSERT INTO salespeople VALUES (1,  'Poirot');
+CREATE TABLE sales_range (
+salesperson_id INT REFERENCES salespeople(salesperson_id),
+sales_amount INT CHECK (sales_amount > 1),
+sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01');
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_feb_mar_apr2022'::regclass::oid ORDER BY conname;
+                        pg_get_constraintdef                         |             conname             | conkey 
+---------------------------------------------------------------------+---------------------------------+--------
+ CHECK ((sales_amount > 1))                                          | sales_range_sales_amount_check  | {2}
+ FOREIGN KEY (salesperson_id) REFERENCES salespeople(salesperson_id) | sales_range_salesperson_id_fkey | {1}
+(2 rows)
+
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+-- We should see the same CONSTRAINTs as on sales_feb_mar_apr2022 partition
+SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_feb2022'::regclass::oid ORDER BY conname;;
+                        pg_get_constraintdef                         |             conname             | conkey 
+---------------------------------------------------------------------+---------------------------------+--------
+ CHECK ((sales_amount > 1))                                          | sales_range_sales_amount_check  | {2}
+ FOREIGN KEY (salesperson_id) REFERENCES salespeople(salesperson_id) | sales_range_salesperson_id_fkey | {1}
+(2 rows)
+
+SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_mar2022'::regclass::oid ORDER BY conname;;
+                        pg_get_constraintdef                         |             conname             | conkey 
+---------------------------------------------------------------------+---------------------------------+--------
+ CHECK ((sales_amount > 1))                                          | sales_range_sales_amount_check  | {2}
+ FOREIGN KEY (salesperson_id) REFERENCES salespeople(salesperson_id) | sales_range_salesperson_id_fkey | {1}
+(2 rows)
+
+SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_apr2022'::regclass::oid ORDER BY conname;;
+                        pg_get_constraintdef                         |             conname             | conkey 
+---------------------------------------------------------------------+---------------------------------+--------
+ CHECK ((sales_amount > 1))                                          | sales_range_sales_amount_check  | {2}
+ FOREIGN KEY (salesperson_id) REFERENCES salespeople(salesperson_id) | sales_range_salesperson_id_fkey | {1}
+(2 rows)
+
+-- ERROR:  new row for relation "sales_mar2022" violates check constraint "sales_range_sales_amount_check"
+INSERT INTO sales_range VALUES (1, 0, '2022-03-11');
+ERROR:  new row for relation "sales_mar2022" violates check constraint "sales_range_sales_amount_check"
+DETAIL:  Failing row contains (1, 0, 03-11-2022).
+-- ERROR:  insert or update on table "sales_mar2022" violates foreign key constraint "sales_range_salesperson_id_fkey"
+INSERT INTO sales_range VALUES (-1, 10, '2022-03-11');
+ERROR:  insert or update on table "sales_mar2022" violates foreign key constraint "sales_range_salesperson_id_fkey"
+DETAIL:  Key (salesperson_id)=(-1) is not present in table "salespeople".
+-- ok
+INSERT INTO sales_range VALUES (1, 10, '2022-03-11');
+DROP TABLE sales_range CASCADE;
+DROP TABLE salespeople CASCADE;
+--
+-- Test: split partition on partitioned table in case of existing FOREIGN KEY reference from another table
+--
+CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id);
+CREATE TABLE sales (salesperson_id INT REFERENCES salespeople(salesperson_id), sales_amount INT, sales_date DATE);
+CREATE TABLE salespeople01_10 PARTITION OF salespeople FOR VALUES FROM (1) TO (10);
+CREATE TABLE salespeople10_40 PARTITION OF salespeople FOR VALUES FROM (10) TO (40);
+INSERT INTO salespeople VALUES (1,  'Poirot');
+INSERT INTO salespeople VALUES (10, 'May');
+INSERT INTO salespeople VALUES (19, 'Ivanov');
+INSERT INTO salespeople VALUES (20, 'Smirnoff');
+INSERT INTO salespeople VALUES (30, 'Ford');
+INSERT INTO sales VALUES (1,  100, '2022-03-01');
+INSERT INTO sales VALUES (1,  110, '2022-03-02');
+INSERT INTO sales VALUES (10, 150, '2022-03-01');
+INSERT INTO sales VALUES (10, 90,  '2022-03-03');
+INSERT INTO sales VALUES (19, 200, '2022-03-04');
+INSERT INTO sales VALUES (20, 50,  '2022-03-12');
+INSERT INTO sales VALUES (20, 170, '2022-03-02');
+INSERT INTO sales VALUES (30, 30,  '2022-03-04');
+SELECT * FROM salespeople01_10;
+ salesperson_id | salesperson_name 
+----------------+------------------
+              1 | Poirot
+(1 row)
+
+SELECT * FROM salespeople10_40;
+ salesperson_id | salesperson_name 
+----------------+------------------
+             10 | May
+             19 | Ivanov
+             20 | Smirnoff
+             30 | Ford
+(4 rows)
+
+ALTER TABLE salespeople SPLIT PARTITION salespeople10_40 INTO
+  (PARTITION salespeople10_20 FOR VALUES FROM (10) TO (20),
+   PARTITION salespeople20_30 FOR VALUES FROM (20) TO (30),
+   PARTITION salespeople30_40 FOR VALUES FROM (30) TO (40));
+SELECT * FROM salespeople01_10;
+ salesperson_id | salesperson_name 
+----------------+------------------
+              1 | Poirot
+(1 row)
+
+SELECT * FROM salespeople10_20;
+ salesperson_id | salesperson_name 
+----------------+------------------
+             10 | May
+             19 | Ivanov
+(2 rows)
+
+SELECT * FROM salespeople20_30;
+ salesperson_id | salesperson_name 
+----------------+------------------
+             20 | Smirnoff
+(1 row)
+
+SELECT * FROM salespeople30_40;
+ salesperson_id | salesperson_name 
+----------------+------------------
+             30 | Ford
+(1 row)
+
+-- ERROR:  insert or update on table "sales" violates foreign key constraint "sales_salesperson_id_fkey"
+INSERT INTO sales VALUES (40, 50,  '2022-03-04');
+ERROR:  insert or update on table "sales" violates foreign key constraint "sales_salesperson_id_fkey"
+DETAIL:  Key (salesperson_id)=(40) is not present in table "salespeople".
+-- ok
+INSERT INTO sales VALUES (30, 50,  '2022-03-04');
+DROP TABLE sales CASCADE;
+DROP TABLE salespeople CASCADE;
+--
+-- Test: split partition of partitioned table with triggers
+--
+CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id);
+CREATE TABLE salespeople01_10 PARTITION OF salespeople FOR VALUES FROM (1) TO (10);
+CREATE TABLE salespeople10_40 PARTITION OF salespeople FOR VALUES FROM (10) TO (40);
+INSERT INTO salespeople VALUES (1,  'Poirot');
+CREATE OR REPLACE FUNCTION after_insert_row_trigger() RETURNS trigger LANGUAGE 'plpgsql' AS $BODY$
+BEGIN
+	RAISE NOTICE 'trigger(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
+    RETURN NULL;
+END;
+$BODY$;
+CREATE TRIGGER salespeople_after_insert_statement_trigger
+    AFTER INSERT
+    ON salespeople
+    FOR EACH STATEMENT
+    EXECUTE PROCEDURE after_insert_row_trigger('salespeople');
+CREATE TRIGGER salespeople_after_insert_row_trigger
+    AFTER INSERT
+    ON salespeople
+    FOR EACH ROW
+    EXECUTE PROCEDURE after_insert_row_trigger('salespeople');
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salespeople VALUES (10, 'May');
+NOTICE:  trigger(salespeople) called: action = INSERT, when = AFTER, level = ROW
+NOTICE:  trigger(salespeople) called: action = INSERT, when = AFTER, level = STATEMENT
+-- 1 trigger should fire here (row):
+INSERT INTO salespeople10_40 VALUES (19, 'Ivanov');
+NOTICE:  trigger(salespeople) called: action = INSERT, when = AFTER, level = ROW
+ALTER TABLE salespeople SPLIT PARTITION salespeople10_40 INTO
+  (PARTITION salespeople10_20 FOR VALUES FROM (10) TO (20),
+   PARTITION salespeople20_30 FOR VALUES FROM (20) TO (30),
+   PARTITION salespeople30_40 FOR VALUES FROM (30) TO (40));
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salespeople VALUES (20, 'Smirnoff');
+NOTICE:  trigger(salespeople) called: action = INSERT, when = AFTER, level = ROW
+NOTICE:  trigger(salespeople) called: action = INSERT, when = AFTER, level = STATEMENT
+-- 1 trigger should fire here (row):
+INSERT INTO salespeople30_40 VALUES (30, 'Ford');
+NOTICE:  trigger(salespeople) called: action = INSERT, when = AFTER, level = ROW
+SELECT * FROM salespeople01_10;
+ salesperson_id | salesperson_name 
+----------------+------------------
+              1 | Poirot
+(1 row)
+
+SELECT * FROM salespeople10_20;
+ salesperson_id | salesperson_name 
+----------------+------------------
+             10 | May
+             19 | Ivanov
+(2 rows)
+
+SELECT * FROM salespeople20_30;
+ salesperson_id | salesperson_name 
+----------------+------------------
+             20 | Smirnoff
+(1 row)
+
+SELECT * FROM salespeople30_40;
+ salesperson_id | salesperson_name 
+----------------+------------------
+             30 | Ford
+(1 row)
+
+DROP TABLE salespeople CASCADE;
+DROP FUNCTION after_insert_row_trigger();
+--
+-- Test: split partition witch identity column
+-- If split partition column is identity column, columns of new partitions are identity columns too.
+--
+CREATE TABLE salespeople(salesperson_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id);
+CREATE TABLE salespeople1_2 PARTITION OF salespeople FOR VALUES FROM (1) TO (2);
+-- Create new partition with identity column:
+CREATE TABLE salespeople2_5(salesperson_id INT NOT NULL, salesperson_name VARCHAR(30));
+ALTER TABLE salespeople ATTACH PARTITION salespeople2_5 FOR VALUES FROM (2) TO (5);
+INSERT INTO salespeople (salesperson_name) VALUES ('Poirot');
+INSERT INTO salespeople (salesperson_name) VALUES ('Ivanov');
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salespeople'::regclass::oid ORDER BY attnum;
+     attname      | attidentity | attgenerated 
+------------------+-------------+--------------
+ salesperson_id   | a           | 
+ salesperson_name |             | 
+(2 rows)
+
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salespeople1_2'::regclass::oid ORDER BY attnum;
+     attname      | attidentity | attgenerated 
+------------------+-------------+--------------
+ salesperson_id   | a           | 
+ salesperson_name |             | 
+(2 rows)
+
+-- Split partition has identity column:
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salespeople2_5'::regclass::oid ORDER BY attnum;
+     attname      | attidentity | attgenerated 
+------------------+-------------+--------------
+ salesperson_id   | a           | 
+ salesperson_name |             | 
+(2 rows)
+
+ALTER TABLE salespeople SPLIT PARTITION salespeople2_5 INTO
+  (PARTITION salespeople2_3 FOR VALUES FROM (2) TO (3),
+   PARTITION salespeople3_4 FOR VALUES FROM (3) TO (4),
+   PARTITION salespeople4_5 FOR VALUES FROM (4) TO (5));
+INSERT INTO salespeople (salesperson_name) VALUES ('May');
+INSERT INTO salespeople (salesperson_name) VALUES ('Ford');
+SELECT * FROM salespeople1_2;
+ salesperson_id | salesperson_name 
+----------------+------------------
+              1 | Poirot
+(1 row)
+
+SELECT * FROM salespeople2_3;
+ salesperson_id | salesperson_name 
+----------------+------------------
+              2 | Ivanov
+(1 row)
+
+SELECT * FROM salespeople3_4;
+ salesperson_id | salesperson_name 
+----------------+------------------
+              3 | May
+(1 row)
+
+SELECT * FROM salespeople4_5;
+ salesperson_id | salesperson_name 
+----------------+------------------
+              4 | Ford
+(1 row)
+
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salespeople'::regclass::oid ORDER BY attnum;
+     attname      | attidentity | attgenerated 
+------------------+-------------+--------------
+ salesperson_id   | a           | 
+ salesperson_name |             | 
+(2 rows)
+
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salespeople1_2'::regclass::oid ORDER BY attnum;
+     attname      | attidentity | attgenerated 
+------------------+-------------+--------------
+ salesperson_id   | a           | 
+ salesperson_name |             | 
+(2 rows)
+
+-- New partitions have identity-columns:
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salespeople2_3'::regclass::oid ORDER BY attnum;
+     attname      | attidentity | attgenerated 
+------------------+-------------+--------------
+ salesperson_id   | a           | 
+ salesperson_name |             | 
+(2 rows)
+
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salespeople3_4'::regclass::oid ORDER BY attnum;
+     attname      | attidentity | attgenerated 
+------------------+-------------+--------------
+ salesperson_id   | a           | 
+ salesperson_name |             | 
+(2 rows)
+
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salespeople4_5'::regclass::oid ORDER BY attnum;
+     attname      | attidentity | attgenerated 
+------------------+-------------+--------------
+ salesperson_id   | a           | 
+ salesperson_name |             | 
+(2 rows)
+
+DROP TABLE salespeople CASCADE;
+--
+-- Test: split partition with deleted columns
+--
+CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id);
+CREATE TABLE salespeople01_10 PARTITION OF salespeople FOR VALUES FROM (1) TO (10);
+-- Create new partition with some deleted columns:
+CREATE TABLE salespeople10_40(d1 VARCHAR(30), salesperson_id INT PRIMARY KEY, d2 INT, d3 DATE, salesperson_name VARCHAR(30));
+INSERT INTO salespeople10_40 VALUES ('dummy value 1', 19, 100, now(), 'Ivanov');
+INSERT INTO salespeople10_40 VALUES ('dummy value 2', 20, 101, now(), 'Smirnoff');
+ALTER TABLE salespeople10_40 DROP COLUMN d1;
+ALTER TABLE salespeople10_40 DROP COLUMN d2;
+ALTER TABLE salespeople10_40 DROP COLUMN d3;
+ALTER TABLE salespeople ATTACH PARTITION salespeople10_40 FOR VALUES FROM (10) TO (40);
+INSERT INTO salespeople VALUES (1, 'Poirot');
+INSERT INTO salespeople VALUES (10, 'May');
+INSERT INTO salespeople VALUES (30, 'Ford');
+ALTER TABLE salespeople SPLIT PARTITION salespeople10_40 INTO
+  (PARTITION salespeople10_20 FOR VALUES FROM (10) TO (20),
+   PARTITION salespeople20_30 FOR VALUES FROM (20) TO (30),
+   PARTITION salespeople30_40 FOR VALUES FROM (30) TO (40));
+select * from salespeople01_10;
+ salesperson_id | salesperson_name 
+----------------+------------------
+              1 | Poirot
+(1 row)
+
+select * from salespeople10_20;
+ salesperson_id | salesperson_name 
+----------------+------------------
+             19 | Ivanov
+             10 | May
+(2 rows)
+
+select * from salespeople20_30;
+ salesperson_id | salesperson_name 
+----------------+------------------
+             20 | Smirnoff
+(1 row)
+
+select * from salespeople30_40;
+ salesperson_id | salesperson_name 
+----------------+------------------
+             30 | Ford
+(1 row)
+
+DROP TABLE salespeople CASCADE;
+--
+-- Test: split sub-partition
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-03-01');
+CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM ('2022-03-01') TO ('2022-04-01');
+CREATE TABLE sales_apr2022 (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_apr_all PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01');
+ALTER TABLE sales_range ATTACH PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01');
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree (sales_date);
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-10');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
+INSERT INTO sales_range VALUES (4,  'Ivanov',   750,  '2022-04-13');
+INSERT INTO sales_range VALUES (5,  'Deev',     250,  '2022-04-07');
+INSERT INTO sales_range VALUES (6,  'Poirot',   150,  '2022-02-11');
+INSERT INTO sales_range VALUES (7,  'Li',       175,  '2022-03-08');
+INSERT INTO sales_range VALUES (8,  'Ericsson', 185,  '2022-02-23');
+INSERT INTO sales_range VALUES (9,  'Muller',   250,  '2022-03-11');
+INSERT INTO sales_range VALUES (10, 'Halder',   350,  '2022-01-28');
+INSERT INTO sales_range VALUES (11, 'Trump',    380,  '2022-04-06');
+INSERT INTO sales_range VALUES (12, 'Plato',    350,  '2022-03-19');
+INSERT INTO sales_range VALUES (13, 'Gandi',    377,  '2022-01-09');
+INSERT INTO sales_range VALUES (14, 'Smith',    510,  '2022-05-04');
+SELECT * FROM sales_range;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              1 | May              |         1000 | 01-31-2022
+             10 | Halder           |          350 | 01-28-2022
+             13 | Gandi            |          377 | 01-09-2022
+              2 | Smirnoff         |          500 | 02-10-2022
+              6 | Poirot           |          150 | 02-11-2022
+              8 | Ericsson         |          185 | 02-23-2022
+              7 | Li               |          175 | 03-08-2022
+              9 | Muller           |          250 | 03-11-2022
+             12 | Plato            |          350 | 03-19-2022
+              3 | Ford             |         2000 | 04-30-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+             14 | Smith            |          510 | 05-04-2022
+(14 rows)
+
+SELECT * FROM sales_apr2022;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              3 | Ford             |         2000 | 04-30-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+(4 rows)
+
+ALTER TABLE sales_apr2022 SPLIT PARTITION sales_apr_all INTO
+  (PARTITION sales_apr2022_01_10 FOR VALUES FROM ('2022-04-01') TO ('2022-04-10'),
+   PARTITION sales_apr2022_10_20 FOR VALUES FROM ('2022-04-10') TO ('2022-04-20'),
+   PARTITION sales_apr2022_20_30 FOR VALUES FROM ('2022-04-20') TO ('2022-05-01'));
+SELECT * FROM sales_range;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              1 | May              |         1000 | 01-31-2022
+             10 | Halder           |          350 | 01-28-2022
+             13 | Gandi            |          377 | 01-09-2022
+              2 | Smirnoff         |          500 | 02-10-2022
+              6 | Poirot           |          150 | 02-11-2022
+              8 | Ericsson         |          185 | 02-23-2022
+              7 | Li               |          175 | 03-08-2022
+              9 | Muller           |          250 | 03-11-2022
+             12 | Plato            |          350 | 03-19-2022
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              3 | Ford             |         2000 | 04-30-2022
+             14 | Smith            |          510 | 05-04-2022
+(14 rows)
+
+SELECT * FROM sales_apr2022;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              3 | Ford             |         2000 | 04-30-2022
+(4 rows)
+
+SELECT * FROM sales_apr2022_01_10;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+(2 rows)
+
+SELECT * FROM sales_apr2022_10_20;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              4 | Ivanov           |          750 | 04-13-2022
+(1 row)
+
+SELECT * FROM sales_apr2022_20_30;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              3 | Ford             |         2000 | 04-30-2022
+(1 row)
+
+DROP TABLE sales_range;
+--
+-- BY LIST partitioning
+--
+--
+-- Test: specific errors for BY LIST partitioning
+--
+CREATE TABLE sales_list
+(salesperson_id INT,
+ salesperson_name VARCHAR(30),
+ sales_state VARCHAR(20),
+ sales_amount INT,
+ sales_date DATE)
+PARTITION BY LIST (sales_state);
+CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki');
+CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Lisbon', 'New York', 'Madrid', 'Bejing', 'Berlin', 'Delhi', 'Kyiv', 'Vladivostok');
+CREATE TABLE sales_others PARTITION OF sales_list DEFAULT;
+-- ERROR:  new partition "sales_east" would overlap with another (not split) partition "sales_nord"
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'),
+   PARTITION sales_east FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok', 'Helsinki'),
+   PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'));
+ERROR:  new partition "sales_east" would overlap with another (not split) partition "sales_nord"
+LINE 3: ... FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok', 'Helsinki'...
+                                                             ^
+-- ERROR:  new partition "sales_west" would overlap with another new partition "sales_central"
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'),
+   PARTITION sales_east FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Lisbon', 'Kyiv'));
+ERROR:  new partition "sales_west" would overlap with another new partition "sales_central"
+LINE 2:   (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York',...
+                                               ^
+-- ERROR:  new partition "sales_west" cannot have NULL value because split partition does not have
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid', NULL),
+   PARTITION sales_east FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'));
+ERROR:  new partition "sales_west" cannot have NULL value because split partition does not have
+LINE 2: ...s_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid', NULL),
+                                                                 ^
+DROP TABLE sales_list;
+--
+-- Test: two specific errors for BY LIST partitioning:
+--   * new partitions do not have NULL value, which split partition has.
+--   * new partitions do not have a value that split partition has.
+--
+CREATE TABLE sales_list
+(salesperson_id INT,
+ salesperson_name VARCHAR(30),
+ sales_state VARCHAR(20),
+ sales_amount INT,
+ sales_date DATE)
+PARTITION BY LIST (sales_state);
+CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Helsinki', 'St. Petersburg', 'Oslo');
+CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Lisbon', 'New York', 'Madrid', 'Bejing', 'Berlin', 'Delhi', 'Kyiv', 'Vladivostok', NULL);
+-- ERROR:  new partitions do not have value NULL but split partition does
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'),
+   PARTITION sales_east FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'));
+ERROR:  new partitions do not have value NULL but split partition does
+-- ERROR:  new partitions do not have value 'Kyiv' but split partition does
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'),
+   PARTITION sales_east FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', NULL));
+ERROR:  new partitions do not have value 'Kyiv' but split partition does
+DROP TABLE sales_list;
+--
+-- Test: BY LIST partitioning, SPLIT PARTITION with data
+--
+CREATE TABLE sales_list
+(salesperson_id SERIAL,
+ salesperson_name VARCHAR(30),
+ sales_state VARCHAR(20),
+ sales_amount INT,
+ sales_date DATE)
+PARTITION BY LIST (sales_state);
+CREATE INDEX sales_list_salesperson_name_idx ON sales_list USING btree (salesperson_name);
+CREATE INDEX sales_list_sales_state_idx ON sales_list USING btree (sales_state);
+CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Helsinki', 'St. Petersburg', 'Oslo');
+CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Lisbon', 'New York', 'Madrid', 'Bejing', 'Berlin', 'Delhi', 'Kyiv', 'Vladivostok');
+CREATE TABLE sales_others PARTITION OF sales_list DEFAULT;
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Trump',    'Bejing',         1000, '2022-03-01');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Smirnoff', 'New York',        500, '2022-03-03');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Ford',     'St. Petersburg', 2000, '2022-03-05');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Ivanov',   'Warsaw',          750, '2022-03-04');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Deev',     'Lisbon',          250, '2022-03-07');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Poirot',   'Berlin',         1000, '2022-03-01');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Oslo',           1200, '2022-03-06');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Li',       'Vladivostok',    1150, '2022-03-09');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Oslo',           1200, '2022-03-11');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Halder',   'Helsinki',        800, '2022-03-02');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Muller',   'Madrid',          650, '2022-03-05');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Smith',    'Kyiv',            350, '2022-03-10');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Gandi',    'Warsaw',          150, '2022-03-08');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Plato',    'Lisbon',          950, '2022-03-05');
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'),
+   PARTITION sales_east FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'));
+SELECT * FROM sales_list;
+ salesperson_id | salesperson_name |  sales_state   | sales_amount | sales_date 
+----------------+------------------+----------------+--------------+------------
+              1 | Trump            | Bejing         |         1000 | 03-01-2022
+              8 | Li               | Vladivostok    |         1150 | 03-09-2022
+              4 | Ivanov           | Warsaw         |          750 | 03-04-2022
+              6 | Poirot           | Berlin         |         1000 | 03-01-2022
+             12 | Smith            | Kyiv           |          350 | 03-10-2022
+             13 | Gandi            | Warsaw         |          150 | 03-08-2022
+              3 | Ford             | St. Petersburg |         2000 | 03-05-2022
+              7 | May              | Oslo           |         1200 | 03-06-2022
+              9 | May              | Oslo           |         1200 | 03-11-2022
+             10 | Halder           | Helsinki       |          800 | 03-02-2022
+              2 | Smirnoff         | New York       |          500 | 03-03-2022
+              5 | Deev             | Lisbon         |          250 | 03-07-2022
+             11 | Muller           | Madrid         |          650 | 03-05-2022
+             14 | Plato            | Lisbon         |          950 | 03-05-2022
+(14 rows)
+
+SELECT * FROM sales_west;
+ salesperson_id | salesperson_name | sales_state | sales_amount | sales_date 
+----------------+------------------+-------------+--------------+------------
+              2 | Smirnoff         | New York    |          500 | 03-03-2022
+              5 | Deev             | Lisbon      |          250 | 03-07-2022
+             11 | Muller           | Madrid      |          650 | 03-05-2022
+             14 | Plato            | Lisbon      |          950 | 03-05-2022
+(4 rows)
+
+SELECT * FROM sales_east;
+ salesperson_id | salesperson_name | sales_state | sales_amount | sales_date 
+----------------+------------------+-------------+--------------+------------
+              1 | Trump            | Bejing      |         1000 | 03-01-2022
+              8 | Li               | Vladivostok |         1150 | 03-09-2022
+(2 rows)
+
+SELECT * FROM sales_nord;
+ salesperson_id | salesperson_name |  sales_state   | sales_amount | sales_date 
+----------------+------------------+----------------+--------------+------------
+              3 | Ford             | St. Petersburg |         2000 | 03-05-2022
+              7 | May              | Oslo           |         1200 | 03-06-2022
+              9 | May              | Oslo           |         1200 | 03-11-2022
+             10 | Halder           | Helsinki       |          800 | 03-02-2022
+(4 rows)
+
+SELECT * FROM sales_central;
+ salesperson_id | salesperson_name | sales_state | sales_amount | sales_date 
+----------------+------------------+-------------+--------------+------------
+              4 | Ivanov           | Warsaw      |          750 | 03-04-2022
+              6 | Poirot           | Berlin      |         1000 | 03-01-2022
+             12 | Smith            | Kyiv        |          350 | 03-10-2022
+             13 | Gandi            | Warsaw      |          150 | 03-08-2022
+(4 rows)
+
+-- Use indexscan for testing indexes after splitting partition
+SET enable_indexscan = ON;
+SET enable_seqscan = OFF;
+SELECT * FROM sales_central WHERE sales_state = 'Warsaw';
+ salesperson_id | salesperson_name | sales_state | sales_amount | sales_date 
+----------------+------------------+-------------+--------------+------------
+              4 | Ivanov           | Warsaw      |          750 | 03-04-2022
+             13 | Gandi            | Warsaw      |          150 | 03-08-2022
+(2 rows)
+
+SELECT * FROM sales_list WHERE sales_state = 'Warsaw';
+ salesperson_id | salesperson_name | sales_state | sales_amount | sales_date 
+----------------+------------------+-------------+--------------+------------
+              4 | Ivanov           | Warsaw      |          750 | 03-04-2022
+             13 | Gandi            | Warsaw      |          150 | 03-08-2022
+(2 rows)
+
+SELECT * FROM sales_list WHERE salesperson_name = 'Ivanov';
+ salesperson_id | salesperson_name | sales_state | sales_amount | sales_date 
+----------------+------------------+-------------+--------------+------------
+              4 | Ivanov           | Warsaw      |          750 | 03-04-2022
+(1 row)
+
+SET enable_indexscan = ON;
+SET enable_seqscan = ON;
+DROP TABLE sales_list;
+--
+-- Test for:
+--   * split DEFAULT partition to partitions with spaces between bounds;
+--   * random order of partitions in SPLIT PARTITION command.
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-09');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
+INSERT INTO sales_range VALUES (4,  'Ivanov',   750,  '2022-04-13');
+INSERT INTO sales_range VALUES (5,  'Deev',     250,  '2022-04-07');
+INSERT INTO sales_range VALUES (6,  'Poirot',   150,  '2022-02-07');
+INSERT INTO sales_range VALUES (7,  'Li',       175,  '2022-03-08');
+INSERT INTO sales_range VALUES (8,  'Ericsson', 185,  '2022-02-23');
+INSERT INTO sales_range VALUES (9,  'Muller',   250,  '2022-03-11');
+INSERT INTO sales_range VALUES (10, 'Halder',   350,  '2022-01-28');
+INSERT INTO sales_range VALUES (11, 'Trump',    380,  '2022-04-06');
+INSERT INTO sales_range VALUES (12, 'Plato',    350,  '2022-03-19');
+INSERT INTO sales_range VALUES (13, 'Gandi',    377,  '2022-01-09');
+INSERT INTO sales_range VALUES (14, 'Smith',    510,  '2022-05-04');
+ALTER TABLE sales_range SPLIT PARTITION sales_others INTO
+  (PARTITION sales_others DEFAULT,
+   PARTITION sales_mar2022_1decade FOR VALUES FROM ('2022-03-01') TO ('2022-03-10'),
+   PARTITION sales_jan2022_1decade FOR VALUES FROM ('2022-01-01') TO ('2022-01-10'),
+   PARTITION sales_feb2022_1decade FOR VALUES FROM ('2022-02-01') TO ('2022-02-10'),
+   PARTITION sales_apr2022_1decade FOR VALUES FROM ('2022-04-01') TO ('2022-04-10'));
+SELECT * FROM sales_jan2022_1decade;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+             13 | Gandi            |          377 | 01-09-2022
+(1 row)
+
+SELECT * FROM sales_feb2022_1decade;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              2 | Smirnoff         |          500 | 02-09-2022
+              6 | Poirot           |          150 | 02-07-2022
+(2 rows)
+
+SELECT * FROM sales_mar2022_1decade;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              7 | Li               |          175 | 03-08-2022
+(1 row)
+
+SELECT * FROM sales_apr2022_1decade;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+(2 rows)
+
+SELECT * FROM sales_others;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              1 | May              |         1000 | 01-31-2022
+              3 | Ford             |         2000 | 04-30-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              8 | Ericsson         |          185 | 02-23-2022
+              9 | Muller           |          250 | 03-11-2022
+             10 | Halder           |          350 | 01-28-2022
+             12 | Plato            |          350 | 03-19-2022
+             14 | Smith            |          510 | 05-04-2022
+(8 rows)
+
+DROP TABLE sales_range;
+--
+-- Test for:
+--   * split non-DEFAULT partition to partitions with spaces between bounds;
+--   * random order of partitions in SPLIT PARTITION command.
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_all PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-05-01');
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-09');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
+INSERT INTO sales_range VALUES (4,  'Ivanov',   750,  '2022-04-13');
+INSERT INTO sales_range VALUES (5,  'Deev',     250,  '2022-04-07');
+INSERT INTO sales_range VALUES (6,  'Poirot',   150,  '2022-02-07');
+INSERT INTO sales_range VALUES (7,  'Li',       175,  '2022-03-08');
+INSERT INTO sales_range VALUES (8,  'Ericsson', 185,  '2022-02-23');
+INSERT INTO sales_range VALUES (9,  'Muller',   250,  '2022-03-11');
+INSERT INTO sales_range VALUES (10, 'Halder',   350,  '2022-01-28');
+INSERT INTO sales_range VALUES (11, 'Trump',    380,  '2022-04-06');
+INSERT INTO sales_range VALUES (12, 'Plato',    350,  '2022-03-19');
+INSERT INTO sales_range VALUES (13, 'Gandi',    377,  '2022-01-09');
+INSERT INTO sales_range VALUES (14, 'Smith',    510,  '2022-05-04');
+ALTER TABLE sales_range SPLIT PARTITION sales_all INTO
+  (PARTITION sales_mar2022_1decade FOR VALUES FROM ('2022-03-01') TO ('2022-03-10'),
+   PARTITION sales_jan2022_1decade FOR VALUES FROM ('2022-01-01') TO ('2022-01-10'),
+   PARTITION sales_feb2022_1decade FOR VALUES FROM ('2022-02-01') TO ('2022-02-10'),
+   PARTITION sales_apr2022_1decade FOR VALUES FROM ('2022-04-01') TO ('2022-04-10'));
+SELECT * FROM sales_jan2022_1decade;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+             13 | Gandi            |          377 | 01-09-2022
+(1 row)
+
+SELECT * FROM sales_feb2022_1decade;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              2 | Smirnoff         |          500 | 02-09-2022
+              6 | Poirot           |          150 | 02-07-2022
+(2 rows)
+
+SELECT * FROM sales_mar2022_1decade;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              7 | Li               |          175 | 03-08-2022
+(1 row)
+
+SELECT * FROM sales_apr2022_1decade;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+(2 rows)
+
+SELECT * FROM sales_others;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+             14 | Smith            |          510 | 05-04-2022
+              1 | May              |         1000 | 01-31-2022
+              3 | Ford             |         2000 | 04-30-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              8 | Ericsson         |          185 | 02-23-2022
+              9 | Muller           |          250 | 03-11-2022
+             10 | Halder           |          350 | 01-28-2022
+             12 | Plato            |          350 | 03-19-2022
+(8 rows)
+
+DROP TABLE sales_range;
+--
+-- Test for split non-DEFAULT partition to DEFAULT partition + partitions
+-- with spaces between bounds.
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_all PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01');
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-10');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
+INSERT INTO sales_range VALUES (4,  'Ivanov',   750,  '2022-04-13');
+INSERT INTO sales_range VALUES (5,  'Deev',     250,  '2022-04-07');
+INSERT INTO sales_range VALUES (6,  'Poirot',   150,  '2022-02-11');
+INSERT INTO sales_range VALUES (7,  'Li',       175,  '2022-03-08');
+INSERT INTO sales_range VALUES (8,  'Ericsson', 185,  '2022-02-23');
+INSERT INTO sales_range VALUES (9,  'Muller',   250,  '2022-03-11');
+INSERT INTO sales_range VALUES (10, 'Halder',   350,  '2022-01-28');
+INSERT INTO sales_range VALUES (11, 'Trump',    380,  '2022-04-06');
+INSERT INTO sales_range VALUES (12, 'Plato',    350,  '2022-03-19');
+INSERT INTO sales_range VALUES (13, 'Gandi',    377,  '2022-01-09');
+ALTER TABLE sales_range SPLIT PARTITION sales_all INTO
+  (PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'),
+   PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_others DEFAULT);
+INSERT INTO sales_range VALUES (14, 'Smith',    510,  '2022-05-04');
+SELECT * FROM sales_range;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              1 | May              |         1000 | 01-31-2022
+             10 | Halder           |          350 | 01-28-2022
+             13 | Gandi            |          377 | 01-09-2022
+              2 | Smirnoff         |          500 | 02-10-2022
+              6 | Poirot           |          150 | 02-11-2022
+              8 | Ericsson         |          185 | 02-23-2022
+              3 | Ford             |         2000 | 04-30-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+              7 | Li               |          175 | 03-08-2022
+              9 | Muller           |          250 | 03-11-2022
+             12 | Plato            |          350 | 03-19-2022
+             14 | Smith            |          510 | 05-04-2022
+(14 rows)
+
+SELECT * FROM sales_jan2022;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              1 | May              |         1000 | 01-31-2022
+             10 | Halder           |          350 | 01-28-2022
+             13 | Gandi            |          377 | 01-09-2022
+(3 rows)
+
+SELECT * FROM sales_feb2022;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              2 | Smirnoff         |          500 | 02-10-2022
+              6 | Poirot           |          150 | 02-11-2022
+              8 | Ericsson         |          185 | 02-23-2022
+(3 rows)
+
+SELECT * FROM sales_apr2022;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              3 | Ford             |         2000 | 04-30-2022
+              4 | Ivanov           |          750 | 04-13-2022
+              5 | Deev             |          250 | 04-07-2022
+             11 | Trump            |          380 | 04-06-2022
+(4 rows)
+
+SELECT * FROM sales_others;
+ salesperson_id | salesperson_name | sales_amount | sales_date 
+----------------+------------------+--------------+------------
+              7 | Li               |          175 | 03-08-2022
+              9 | Muller           |          250 | 03-11-2022
+             12 | Plato            |          350 | 03-19-2022
+             14 | Smith            |          510 | 05-04-2022
+(4 rows)
+
+DROP TABLE sales_range;
+--
+-- Try to SPLIT partition of another table.
+--
+CREATE TABLE t1(i int, t text) PARTITION BY LIST (t);
+CREATE TABLE t1pa PARTITION OF t1 FOR VALUES IN ('A');
+CREATE TABLE t2 (i int, t text) PARTITION BY RANGE (t);
+-- ERROR:  relation "t1pa" is not a partition of relation "t2"
+ALTER TABLE t2 SPLIT PARTITION t1pa INTO
+   (PARTITION t2a FOR VALUES FROM ('A') TO ('B'),
+    PARTITION t2b FOR VALUES FROM ('B') TO ('C'));
+ERROR:  relation "t1pa" is not a partition of relation "t2"
+DROP TABLE t2;
+DROP TABLE t1;
+--
+-- Try to SPLIT partition of temporary table.
+--
+CREATE TEMP TABLE t (i int) PARTITION BY RANGE (i);
+CREATE TEMP TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (2);
+SELECT c.oid::pg_catalog.regclass, pg_catalog.pg_get_expr(c.relpartbound, c.oid), c.relpersistence
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 't'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+  oid   |        pg_get_expr         | relpersistence 
+--------+----------------------------+----------------
+ tp_0_2 | FOR VALUES FROM (0) TO (2) | t
+(1 row)
+
+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));
+-- Partitions should be temporary.
+SELECT c.oid::pg_catalog.regclass, pg_catalog.pg_get_expr(c.relpartbound, c.oid), c.relpersistence
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 't'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+  oid   |        pg_get_expr         | relpersistence 
+--------+----------------------------+----------------
+ tp_0_1 | FOR VALUES FROM (0) TO (1) | t
+ tp_1_2 | FOR VALUES FROM (1) TO (2) | t
+(2 rows)
+
+DROP TABLE t;
+-- Check the new partitions inherit parent's tablespace
+CREATE TABLE t (i int PRIMARY KEY USING INDEX TABLESPACE regress_tblspace)
+  PARTITION BY RANGE (i) TABLESPACE regress_tblspace;
+CREATE TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (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));
+SELECT tablename, tablespace FROM pg_tables
+  WHERE tablename IN ('t', 'tp_0_1', 'tp_1_2') AND schemaname = 'partition_split_schema'
+  ORDER BY tablename, tablespace;
+ tablename |    tablespace    
+-----------+------------------
+ t         | regress_tblspace
+ tp_0_1    | regress_tblspace
+ tp_1_2    | regress_tblspace
+(3 rows)
+
+SELECT tablename, indexname, tablespace FROM pg_indexes
+  WHERE tablename IN ('t', 'tp_0_1', 'tp_1_2') AND schemaname = 'partition_split_schema'
+  ORDER BY tablename, indexname, tablespace;
+ tablename |  indexname  |    tablespace    
+-----------+-------------+------------------
+ t         | t_pkey      | regress_tblspace
+ tp_0_1    | tp_0_1_pkey | regress_tblspace
+ tp_1_2    | tp_1_2_pkey | regress_tblspace
+(3 rows)
+
+DROP TABLE t;
+-- Check new partitions inherits parent's table access method
+CREATE ACCESS METHOD partition_split_heap TYPE TABLE HANDLER heap_tableam_handler;
+CREATE TABLE t (i int) PARTITION BY RANGE (i) USING partition_split_heap;
+CREATE TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (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));
+SELECT c.relname, a.amname
+FROM pg_class c JOIN pg_am a ON c.relam = a.oid
+WHERE c.oid IN ('t'::regclass, 'tp_0_1'::regclass, 'tp_1_2'::regclass)
+ORDER BY c.relname;
+ relname |        amname        
+---------+----------------------
+ t       | partition_split_heap
+ tp_0_1  | partition_split_heap
+ tp_1_2  | partition_split_heap
+(3 rows)
+
+DROP TABLE t;
+DROP ACCESS METHOD partition_split_heap;
+-- Test permission checks.  The user needs to own the parent table and the
+-- the partition to split to do the split.
+CREATE ROLE regress_partition_split_alice;
+CREATE ROLE regress_partition_split_bob;
+GRANT ALL ON SCHEMA partition_split_schema TO regress_partition_split_alice;
+GRANT ALL ON SCHEMA partition_split_schema TO regress_partition_split_bob;
+SET SESSION AUTHORIZATION regress_partition_split_alice;
+CREATE TABLE t (i int) PARTITION BY RANGE (i);
+CREATE TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (2);
+SET SESSION AUTHORIZATION regress_partition_split_bob;
+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));
+ERROR:  must be owner of table t
+RESET SESSION AUTHORIZATION;
+ALTER TABLE t OWNER TO regress_partition_split_bob;
+SET SESSION AUTHORIZATION regress_partition_split_bob;
+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));
+ERROR:  must be owner of table tp_0_2
+RESET SESSION AUTHORIZATION;
+ALTER TABLE tp_0_2 OWNER TO regress_partition_split_bob;
+SET SESSION AUTHORIZATION regress_partition_split_bob;
+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));
+RESET SESSION AUTHORIZATION;
+DROP TABLE t;
+REVOKE ALL ON SCHEMA partition_split_schema FROM regress_partition_split_alice;
+REVOKE ALL ON SCHEMA partition_split_schema FROM regress_partition_split_bob;
+DROP ROLE regress_partition_split_alice;
+DROP ROLE regress_partition_split_bob;
+-- Split partition of a temporary table when one of the partitions after
+-- split has the same name as the partition being split
+CREATE TEMP TABLE t (a int) PARTITION BY RANGE (a);
+CREATE TEMP TABLE tp_0 PARTITION OF t FOR VALUES FROM (0) TO (2);
+ALTER TABLE t SPLIT PARTITION tp_0 INTO
+  (PARTITION tp_0 FOR VALUES FROM (0) TO (1),
+   PARTITION tp_1 FOR VALUES FROM (1) TO (2));
+DROP TABLE t;
+RESET search_path;
+--
+DROP SCHEMA partition_split_schema;
+DROP SCHEMA partition_split_schema2;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 0014801684..2429ec2bba 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -119,7 +119,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # The stats test resets stats, so nothing else needing stats access can be in
 # this group.
 # ----------
-test: partition_merge partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate
+test: partition_merge partition_split partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate
 
 # event_trigger depends on create_am and cannot run concurrently with
 # any test that runs DDL
diff --git a/src/test/regress/sql/partition_split.sql b/src/test/regress/sql/partition_split.sql
new file mode 100644
index 0000000000..ef5ea07f74
--- /dev/null
+++ b/src/test/regress/sql/partition_split.sql
@@ -0,0 +1,962 @@
+--
+-- PARTITION_SPLIT
+-- Tests for "ALTER TABLE ... SPLIT PARTITION ..." command
+--
+
+CREATE SCHEMA partition_split_schema;
+CREATE SCHEMA partition_split_schema2;
+SET search_path = partition_split_schema, public;
+
+--
+-- BY RANGE partitioning
+--
+
+--
+-- Test for error codes
+--
+CREATE TABLE sales_range (salesperson_id int, salesperson_name varchar(30), sales_amount int, sales_date date) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01');
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+
+-- ERROR:  relation "sales_xxx" does not exist
+ALTER TABLE sales_range SPLIT PARTITION sales_xxx INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+
+-- ERROR:  relation "sales_jan2022" already exists
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_jan2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+
+-- ERROR:  invalid bound specification for a range partition
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_jan2022 FOR VALUES IN ('2022-05-01', '2022-06-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+
+-- ERROR:  empty range bound specified for partition "sales_mar2022"
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-02-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+
+--ERROR:  list of split partitions should contain at least two items
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-10-01'));
+
+-- ERROR:  lower bound of partition "sales_feb2022" is less than lower bound of split partition
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-01-01') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+
+-- ERROR:  name "sales_feb_mar_apr2022" is already used
+-- (We can create partition with the same name as split partition, but can't create two partitions with the same name)
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb_mar_apr2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_feb_mar_apr2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+
+-- ERROR:  name "sales_feb2022" is already used
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_feb2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+
+-- ERROR:  "sales_feb_mar_apr2022" is not a partitioned table
+ALTER TABLE sales_feb_mar_apr2022 SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_jan2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_feb2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+
+-- ERROR:  upper bound of partition "sales_apr2022" is greater than upper bound of split partition
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-06-01'));
+
+-- ERROR:  lower bound of partition "sales_mar2022" conflicts with upper bound of previous partition "sales_feb2022"
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-02-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+
+-- Tests for spaces between partitions, them should be executed without DEFAULT partition
+ALTER TABLE sales_range DETACH PARTITION sales_others;
+
+-- ERROR:  lower bound of partition "sales_feb2022" is not equal to lower bound of split partition
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-02') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+
+-- Check the source partition not in the search path
+SET search_path = partition_split_schema2, public;
+ALTER TABLE partition_split_schema.sales_range
+SPLIT PARTITION partition_split_schema.sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+SET search_path = partition_split_schema, public;
+\d+ sales_range
+
+DROP TABLE sales_range;
+DROP TABLE sales_others;
+
+--
+-- Add rows into partitioned table then split partition
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01');
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-10');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
+INSERT INTO sales_range VALUES (4,  'Ivanov',   750,  '2022-04-13');
+INSERT INTO sales_range VALUES (5,  'Deev',     250,  '2022-04-07');
+INSERT INTO sales_range VALUES (6,  'Poirot',   150,  '2022-02-11');
+INSERT INTO sales_range VALUES (7,  'Li',       175,  '2022-03-08');
+INSERT INTO sales_range VALUES (8,  'Ericsson', 185,  '2022-02-23');
+INSERT INTO sales_range VALUES (9,  'Muller',   250,  '2022-03-11');
+INSERT INTO sales_range VALUES (10, 'Halder',   350,  '2022-01-28');
+INSERT INTO sales_range VALUES (11, 'Trump',    380,  '2022-04-06');
+INSERT INTO sales_range VALUES (12, 'Plato',    350,  '2022-03-19');
+INSERT INTO sales_range VALUES (13, 'Gandi',    377,  '2022-01-09');
+INSERT INTO sales_range VALUES (14, 'Smith',    510,  '2022-05-04');
+
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+
+SELECT * FROM sales_range;
+SELECT * FROM sales_jan2022;
+SELECT * FROM sales_feb2022;
+SELECT * FROM sales_mar2022;
+SELECT * FROM sales_apr2022;
+SELECT * FROM sales_others;
+
+DROP TABLE sales_range CASCADE;
+
+--
+-- Add split partition, then add rows into partitioned table
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01');
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+
+-- Split partition, also check schema qualification of new partitions
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION partition_split_schema.sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION partition_split_schema2.sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+\d+ sales_range
+
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-10');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
+INSERT INTO sales_range VALUES (4,  'Ivanov',   750,  '2022-04-13');
+INSERT INTO sales_range VALUES (5,  'Deev',     250,  '2022-04-07');
+INSERT INTO sales_range VALUES (6,  'Poirot',   150,  '2022-02-11');
+INSERT INTO sales_range VALUES (7,  'Li',       175,  '2022-03-08');
+INSERT INTO sales_range VALUES (8,  'Ericsson', 185,  '2022-02-23');
+INSERT INTO sales_range VALUES (9,  'Muller',   250,  '2022-03-11');
+INSERT INTO sales_range VALUES (10, 'Halder',   350,  '2022-01-28');
+INSERT INTO sales_range VALUES (11, 'Trump',    380,  '2022-04-06');
+INSERT INTO sales_range VALUES (12, 'Plato',    350,  '2022-03-19');
+INSERT INTO sales_range VALUES (13, 'Gandi',    377,  '2022-01-09');
+INSERT INTO sales_range VALUES (14, 'Smith',    510,  '2022-05-04');
+
+SELECT * FROM sales_range;
+SELECT * FROM sales_jan2022;
+SELECT * FROM sales_feb2022;
+SELECT * FROM partition_split_schema2.sales_mar2022;
+SELECT * FROM sales_apr2022;
+SELECT * FROM sales_others;
+
+DROP TABLE sales_range CASCADE;
+
+--
+-- Test for:
+--   * composite partition key;
+--   * GENERATED column;
+--   * column with DEFAULT value.
+--
+CREATE TABLE sales_date (salesperson_name VARCHAR(30), sales_year INT, sales_month INT, sales_day INT,
+  sales_date VARCHAR(10) GENERATED ALWAYS AS
+    (LPAD(sales_year::text, 4, '0') || '.' || LPAD(sales_month::text, 2, '0') || '.' || LPAD(sales_day::text, 2, '0')) STORED,
+  sales_department VARCHAR(30) DEFAULT 'Sales department')
+  PARTITION BY RANGE (sales_year, sales_month, sales_day);
+
+CREATE TABLE sales_dec2022 PARTITION OF sales_date FOR VALUES FROM (2021, 12, 1) TO (2022, 1, 1);
+CREATE TABLE sales_jan_feb2022 PARTITION OF sales_date FOR VALUES FROM (2022, 1, 1) TO (2022, 3, 1);
+CREATE TABLE sales_other PARTITION OF sales_date FOR VALUES FROM (2022, 3, 1) TO (MAXVALUE, MAXVALUE, MAXVALUE);
+
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2021, 12, 7);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2021, 12, 8);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 1, 1);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 2, 4);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 1, 2);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 2, 1);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 3, 3);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 3, 4);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 5, 1);
+
+SELECT * FROM sales_date;
+SELECT * FROM sales_dec2022;
+SELECT * FROM sales_jan_feb2022;
+SELECT * FROM sales_other;
+
+ALTER TABLE sales_date SPLIT PARTITION sales_jan_feb2022 INTO
+  (PARTITION sales_jan2022 FOR VALUES FROM (2022, 1, 1) TO (2022, 2, 1),
+   PARTITION sales_feb2022 FOR VALUES FROM (2022, 2, 1) TO (2022, 3, 1));
+
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 1, 10);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 2, 10);
+
+SELECT * FROM sales_date;
+SELECT * FROM sales_dec2022;
+SELECT * FROM sales_jan2022;
+SELECT * FROM sales_feb2022;
+SELECT * FROM sales_other;
+
+--ERROR:  relation "sales_jan_feb2022" does not exist
+SELECT * FROM sales_jan_feb2022;
+
+DROP TABLE sales_date CASCADE;
+
+--
+-- Test: split DEFAULT partition; use an index on partition key; check index after split
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree (sales_date);
+
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-10');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
+INSERT INTO sales_range VALUES (4,  'Ivanov',   750,  '2022-04-13');
+INSERT INTO sales_range VALUES (5,  'Deev',     250,  '2022-04-07');
+INSERT INTO sales_range VALUES (6,  'Poirot',   150,  '2022-02-11');
+INSERT INTO sales_range VALUES (7,  'Li',       175,  '2022-03-08');
+INSERT INTO sales_range VALUES (8,  'Ericsson', 185,  '2022-02-23');
+INSERT INTO sales_range VALUES (9,  'Muller',   250,  '2022-03-11');
+INSERT INTO sales_range VALUES (10, 'Halder',   350,  '2022-01-28');
+INSERT INTO sales_range VALUES (11, 'Trump',    380,  '2022-04-06');
+INSERT INTO sales_range VALUES (12, 'Plato',    350,  '2022-03-19');
+INSERT INTO sales_range VALUES (13, 'Gandi',    377,  '2022-01-09');
+INSERT INTO sales_range VALUES (14, 'Smith',    510,  '2022-05-04');
+
+SELECT * FROM sales_others;
+SELECT * FROM pg_indexes WHERE tablename = 'sales_others' and schemaname = 'partition_split_schema' ORDER BY indexname;
+
+ALTER TABLE sales_range SPLIT PARTITION sales_others INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'),
+   PARTITION sales_others DEFAULT);
+
+-- Use indexscan for testing indexes
+SET enable_indexscan = ON;
+SET enable_seqscan = OFF;
+
+SELECT * FROM sales_feb2022 where sales_date > '2022-01-01';
+SELECT * FROM sales_mar2022 where sales_date > '2022-01-01';
+SELECT * FROM sales_apr2022 where sales_date > '2022-01-01';
+SELECT * FROM sales_others where sales_date > '2022-01-01';
+
+SET enable_indexscan = ON;
+SET enable_seqscan = ON;
+
+SELECT * FROM pg_indexes WHERE tablename = 'sales_feb2022' and schemaname = 'partition_split_schema' ORDER BY indexname;
+SELECT * FROM pg_indexes WHERE tablename = 'sales_mar2022' and schemaname = 'partition_split_schema' ORDER BY indexname;
+SELECT * FROM pg_indexes WHERE tablename = 'sales_apr2022' and schemaname = 'partition_split_schema' ORDER BY indexname;
+SELECT * FROM pg_indexes WHERE tablename = 'sales_others' and schemaname = 'partition_split_schema' ORDER BY indexname;
+
+DROP TABLE sales_range CASCADE;
+
+--
+-- Test: some cases for splitting DEFAULT partition (different bounds)
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date INT) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+
+-- sales_error intersects with sales_dec2022 (lower bound)
+-- ERROR:  lower bound of partition "sales_error" conflicts with upper bound of previous partition "sales_dec2022"
+ALTER TABLE sales_range SPLIT PARTITION sales_others INTO
+  (PARTITION sales_dec2022 FOR VALUES FROM (20211201) TO (20220101),
+   PARTITION sales_error FOR VALUES FROM (20211230) TO (20220201),
+   PARTITION sales_feb2022 FOR VALUES FROM (20220201) TO (20220301),
+   PARTITION sales_others DEFAULT);
+
+-- sales_error intersects with sales_feb2022 (upper bound)
+-- ERROR:  lower bound of partition "sales_feb2022" conflicts with upper bound of previous partition "sales_error"
+ALTER TABLE sales_range SPLIT PARTITION sales_others INTO
+  (PARTITION sales_dec2022 FOR VALUES FROM (20211201) TO (20220101),
+   PARTITION sales_error FOR VALUES FROM (20220101) TO (20220202),
+   PARTITION sales_feb2022 FOR VALUES FROM (20220201) TO (20220301),
+   PARTITION sales_others DEFAULT);
+
+-- sales_error intersects with sales_dec2022 (inside bound)
+-- ERROR:  lower bound of partition "sales_error" conflicts with upper bound of previous partition "sales_dec2022"
+ALTER TABLE sales_range SPLIT PARTITION sales_others INTO
+  (PARTITION sales_dec2022 FOR VALUES FROM (20211201) TO (20220101),
+   PARTITION sales_error FOR VALUES FROM (20211210) TO (20211220),
+   PARTITION sales_feb2022 FOR VALUES FROM (20220201) TO (20220301),
+   PARTITION sales_others DEFAULT);
+
+-- sales_error intersects with sales_dec2022 (exactly the same bounds)
+-- ERROR:  lower bound of partition "sales_error" conflicts with upper bound of previous partition "sales_dec2022"
+ALTER TABLE sales_range SPLIT PARTITION sales_others INTO
+  (PARTITION sales_dec2022 FOR VALUES FROM (20211201) TO (20220101),
+   PARTITION sales_error FOR VALUES FROM (20211201) TO (20220101),
+   PARTITION sales_feb2022 FOR VALUES FROM (20220201) TO (20220301),
+   PARTITION sales_others DEFAULT);
+
+-- ERROR:  all partitions in the list should be DEFAULT because split partition is DEFAULT
+ALTER TABLE sales_range SPLIT PARTITION sales_others INTO
+  (PARTITION sales_dec2022 FOR VALUES FROM (20211201) TO (20220101),
+   PARTITION sales_jan2022 FOR VALUES FROM (20220101) TO (20220201),
+   PARTITION sales_feb2022 FOR VALUES FROM (20220201) TO (20220301));
+
+-- no error: bounds of sales_noerror are between sales_dec2022 and sales_feb2022
+ALTER TABLE sales_range SPLIT PARTITION sales_others INTO
+  (PARTITION sales_dec2022 FOR VALUES FROM (20211201) TO (20220101),
+   PARTITION sales_noerror FOR VALUES FROM (20220110) TO (20220120),
+   PARTITION sales_feb2022 FOR VALUES FROM (20220201) TO (20220301),
+   PARTITION sales_others DEFAULT);
+
+DROP TABLE sales_range;
+
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date INT) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+
+-- no error: bounds of sales_noerror are equal to lower and upper bounds of sales_dec2022 and sales_feb2022
+ALTER TABLE sales_range SPLIT PARTITION sales_others INTO
+  (PARTITION sales_dec2022 FOR VALUES FROM (20211201) TO (20220101),
+   PARTITION sales_noerror FOR VALUES FROM (20210101) TO (20210201),
+   PARTITION sales_feb2022 FOR VALUES FROM (20220201) TO (20220301),
+   PARTITION sales_others DEFAULT);
+
+DROP TABLE sales_range;
+
+--
+-- Test: split partition with CHECK and FOREIGN KEY CONSTRAINTs on partitioned table
+--
+CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30));
+INSERT INTO salespeople VALUES (1,  'Poirot');
+
+CREATE TABLE sales_range (
+salesperson_id INT REFERENCES salespeople(salesperson_id),
+sales_amount INT CHECK (sales_amount > 1),
+sales_date DATE) PARTITION BY RANGE (sales_date);
+
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01');
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+
+SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_feb_mar_apr2022'::regclass::oid ORDER BY conname;
+
+ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO
+  (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'),
+   PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'));
+
+-- We should see the same CONSTRAINTs as on sales_feb_mar_apr2022 partition
+SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_feb2022'::regclass::oid ORDER BY conname;;
+SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_mar2022'::regclass::oid ORDER BY conname;;
+SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_apr2022'::regclass::oid ORDER BY conname;;
+
+-- ERROR:  new row for relation "sales_mar2022" violates check constraint "sales_range_sales_amount_check"
+INSERT INTO sales_range VALUES (1, 0, '2022-03-11');
+-- ERROR:  insert or update on table "sales_mar2022" violates foreign key constraint "sales_range_salesperson_id_fkey"
+INSERT INTO sales_range VALUES (-1, 10, '2022-03-11');
+-- ok
+INSERT INTO sales_range VALUES (1, 10, '2022-03-11');
+
+DROP TABLE sales_range CASCADE;
+DROP TABLE salespeople CASCADE;
+
+--
+-- Test: split partition on partitioned table in case of existing FOREIGN KEY reference from another table
+--
+CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id);
+CREATE TABLE sales (salesperson_id INT REFERENCES salespeople(salesperson_id), sales_amount INT, sales_date DATE);
+
+CREATE TABLE salespeople01_10 PARTITION OF salespeople FOR VALUES FROM (1) TO (10);
+CREATE TABLE salespeople10_40 PARTITION OF salespeople FOR VALUES FROM (10) TO (40);
+
+INSERT INTO salespeople VALUES (1,  'Poirot');
+INSERT INTO salespeople VALUES (10, 'May');
+INSERT INTO salespeople VALUES (19, 'Ivanov');
+INSERT INTO salespeople VALUES (20, 'Smirnoff');
+INSERT INTO salespeople VALUES (30, 'Ford');
+
+INSERT INTO sales VALUES (1,  100, '2022-03-01');
+INSERT INTO sales VALUES (1,  110, '2022-03-02');
+INSERT INTO sales VALUES (10, 150, '2022-03-01');
+INSERT INTO sales VALUES (10, 90,  '2022-03-03');
+INSERT INTO sales VALUES (19, 200, '2022-03-04');
+INSERT INTO sales VALUES (20, 50,  '2022-03-12');
+INSERT INTO sales VALUES (20, 170, '2022-03-02');
+INSERT INTO sales VALUES (30, 30,  '2022-03-04');
+
+SELECT * FROM salespeople01_10;
+SELECT * FROM salespeople10_40;
+
+ALTER TABLE salespeople SPLIT PARTITION salespeople10_40 INTO
+  (PARTITION salespeople10_20 FOR VALUES FROM (10) TO (20),
+   PARTITION salespeople20_30 FOR VALUES FROM (20) TO (30),
+   PARTITION salespeople30_40 FOR VALUES FROM (30) TO (40));
+
+SELECT * FROM salespeople01_10;
+SELECT * FROM salespeople10_20;
+SELECT * FROM salespeople20_30;
+SELECT * FROM salespeople30_40;
+
+-- ERROR:  insert or update on table "sales" violates foreign key constraint "sales_salesperson_id_fkey"
+INSERT INTO sales VALUES (40, 50,  '2022-03-04');
+-- ok
+INSERT INTO sales VALUES (30, 50,  '2022-03-04');
+
+DROP TABLE sales CASCADE;
+DROP TABLE salespeople CASCADE;
+
+--
+-- Test: split partition of partitioned table with triggers
+--
+CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id);
+
+CREATE TABLE salespeople01_10 PARTITION OF salespeople FOR VALUES FROM (1) TO (10);
+CREATE TABLE salespeople10_40 PARTITION OF salespeople FOR VALUES FROM (10) TO (40);
+
+INSERT INTO salespeople VALUES (1,  'Poirot');
+
+CREATE OR REPLACE FUNCTION after_insert_row_trigger() RETURNS trigger LANGUAGE 'plpgsql' AS $BODY$
+BEGIN
+	RAISE NOTICE 'trigger(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
+    RETURN NULL;
+END;
+$BODY$;
+
+CREATE TRIGGER salespeople_after_insert_statement_trigger
+    AFTER INSERT
+    ON salespeople
+    FOR EACH STATEMENT
+    EXECUTE PROCEDURE after_insert_row_trigger('salespeople');
+
+CREATE TRIGGER salespeople_after_insert_row_trigger
+    AFTER INSERT
+    ON salespeople
+    FOR EACH ROW
+    EXECUTE PROCEDURE after_insert_row_trigger('salespeople');
+
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salespeople VALUES (10, 'May');
+-- 1 trigger should fire here (row):
+INSERT INTO salespeople10_40 VALUES (19, 'Ivanov');
+
+ALTER TABLE salespeople SPLIT PARTITION salespeople10_40 INTO
+  (PARTITION salespeople10_20 FOR VALUES FROM (10) TO (20),
+   PARTITION salespeople20_30 FOR VALUES FROM (20) TO (30),
+   PARTITION salespeople30_40 FOR VALUES FROM (30) TO (40));
+
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salespeople VALUES (20, 'Smirnoff');
+-- 1 trigger should fire here (row):
+INSERT INTO salespeople30_40 VALUES (30, 'Ford');
+
+SELECT * FROM salespeople01_10;
+SELECT * FROM salespeople10_20;
+SELECT * FROM salespeople20_30;
+SELECT * FROM salespeople30_40;
+
+DROP TABLE salespeople CASCADE;
+DROP FUNCTION after_insert_row_trigger();
+
+--
+-- Test: split partition witch identity column
+-- If split partition column is identity column, columns of new partitions are identity columns too.
+--
+CREATE TABLE salespeople(salesperson_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id);
+
+CREATE TABLE salespeople1_2 PARTITION OF salespeople FOR VALUES FROM (1) TO (2);
+-- Create new partition with identity column:
+CREATE TABLE salespeople2_5(salesperson_id INT NOT NULL, salesperson_name VARCHAR(30));
+ALTER TABLE salespeople ATTACH PARTITION salespeople2_5 FOR VALUES FROM (2) TO (5);
+
+INSERT INTO salespeople (salesperson_name) VALUES ('Poirot');
+INSERT INTO salespeople (salesperson_name) VALUES ('Ivanov');
+
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salespeople'::regclass::oid ORDER BY attnum;
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salespeople1_2'::regclass::oid ORDER BY attnum;
+-- Split partition has identity column:
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salespeople2_5'::regclass::oid ORDER BY attnum;
+
+ALTER TABLE salespeople SPLIT PARTITION salespeople2_5 INTO
+  (PARTITION salespeople2_3 FOR VALUES FROM (2) TO (3),
+   PARTITION salespeople3_4 FOR VALUES FROM (3) TO (4),
+   PARTITION salespeople4_5 FOR VALUES FROM (4) TO (5));
+
+INSERT INTO salespeople (salesperson_name) VALUES ('May');
+INSERT INTO salespeople (salesperson_name) VALUES ('Ford');
+
+SELECT * FROM salespeople1_2;
+SELECT * FROM salespeople2_3;
+SELECT * FROM salespeople3_4;
+SELECT * FROM salespeople4_5;
+
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salespeople'::regclass::oid ORDER BY attnum;
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salespeople1_2'::regclass::oid ORDER BY attnum;
+-- New partitions have identity-columns:
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salespeople2_3'::regclass::oid ORDER BY attnum;
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salespeople3_4'::regclass::oid ORDER BY attnum;
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salespeople4_5'::regclass::oid ORDER BY attnum;
+
+DROP TABLE salespeople CASCADE;
+
+--
+-- Test: split partition with deleted columns
+--
+CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id);
+
+CREATE TABLE salespeople01_10 PARTITION OF salespeople FOR VALUES FROM (1) TO (10);
+-- Create new partition with some deleted columns:
+CREATE TABLE salespeople10_40(d1 VARCHAR(30), salesperson_id INT PRIMARY KEY, d2 INT, d3 DATE, salesperson_name VARCHAR(30));
+
+INSERT INTO salespeople10_40 VALUES ('dummy value 1', 19, 100, now(), 'Ivanov');
+INSERT INTO salespeople10_40 VALUES ('dummy value 2', 20, 101, now(), 'Smirnoff');
+
+ALTER TABLE salespeople10_40 DROP COLUMN d1;
+ALTER TABLE salespeople10_40 DROP COLUMN d2;
+ALTER TABLE salespeople10_40 DROP COLUMN d3;
+
+ALTER TABLE salespeople ATTACH PARTITION salespeople10_40 FOR VALUES FROM (10) TO (40);
+
+INSERT INTO salespeople VALUES (1, 'Poirot');
+INSERT INTO salespeople VALUES (10, 'May');
+INSERT INTO salespeople VALUES (30, 'Ford');
+
+ALTER TABLE salespeople SPLIT PARTITION salespeople10_40 INTO
+  (PARTITION salespeople10_20 FOR VALUES FROM (10) TO (20),
+   PARTITION salespeople20_30 FOR VALUES FROM (20) TO (30),
+   PARTITION salespeople30_40 FOR VALUES FROM (30) TO (40));
+
+select * from salespeople01_10;
+select * from salespeople10_20;
+select * from salespeople20_30;
+select * from salespeople30_40;
+
+DROP TABLE salespeople CASCADE;
+
+--
+-- Test: split sub-partition
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-03-01');
+CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM ('2022-03-01') TO ('2022-04-01');
+
+CREATE TABLE sales_apr2022 (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_apr_all PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01');
+ALTER TABLE sales_range ATTACH PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01');
+
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+
+CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree (sales_date);
+
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-10');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
+INSERT INTO sales_range VALUES (4,  'Ivanov',   750,  '2022-04-13');
+INSERT INTO sales_range VALUES (5,  'Deev',     250,  '2022-04-07');
+INSERT INTO sales_range VALUES (6,  'Poirot',   150,  '2022-02-11');
+INSERT INTO sales_range VALUES (7,  'Li',       175,  '2022-03-08');
+INSERT INTO sales_range VALUES (8,  'Ericsson', 185,  '2022-02-23');
+INSERT INTO sales_range VALUES (9,  'Muller',   250,  '2022-03-11');
+INSERT INTO sales_range VALUES (10, 'Halder',   350,  '2022-01-28');
+INSERT INTO sales_range VALUES (11, 'Trump',    380,  '2022-04-06');
+INSERT INTO sales_range VALUES (12, 'Plato',    350,  '2022-03-19');
+INSERT INTO sales_range VALUES (13, 'Gandi',    377,  '2022-01-09');
+INSERT INTO sales_range VALUES (14, 'Smith',    510,  '2022-05-04');
+
+SELECT * FROM sales_range;
+SELECT * FROM sales_apr2022;
+
+ALTER TABLE sales_apr2022 SPLIT PARTITION sales_apr_all INTO
+  (PARTITION sales_apr2022_01_10 FOR VALUES FROM ('2022-04-01') TO ('2022-04-10'),
+   PARTITION sales_apr2022_10_20 FOR VALUES FROM ('2022-04-10') TO ('2022-04-20'),
+   PARTITION sales_apr2022_20_30 FOR VALUES FROM ('2022-04-20') TO ('2022-05-01'));
+
+SELECT * FROM sales_range;
+SELECT * FROM sales_apr2022;
+SELECT * FROM sales_apr2022_01_10;
+SELECT * FROM sales_apr2022_10_20;
+SELECT * FROM sales_apr2022_20_30;
+
+DROP TABLE sales_range;
+
+--
+-- BY LIST partitioning
+--
+
+--
+-- Test: specific errors for BY LIST partitioning
+--
+CREATE TABLE sales_list
+(salesperson_id INT,
+ salesperson_name VARCHAR(30),
+ sales_state VARCHAR(20),
+ sales_amount INT,
+ sales_date DATE)
+PARTITION BY LIST (sales_state);
+
+CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki');
+CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Lisbon', 'New York', 'Madrid', 'Bejing', 'Berlin', 'Delhi', 'Kyiv', 'Vladivostok');
+CREATE TABLE sales_others PARTITION OF sales_list DEFAULT;
+
+-- ERROR:  new partition "sales_east" would overlap with another (not split) partition "sales_nord"
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'),
+   PARTITION sales_east FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok', 'Helsinki'),
+   PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'));
+
+-- ERROR:  new partition "sales_west" would overlap with another new partition "sales_central"
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'),
+   PARTITION sales_east FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Lisbon', 'Kyiv'));
+
+-- ERROR:  new partition "sales_west" cannot have NULL value because split partition does not have
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid', NULL),
+   PARTITION sales_east FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'));
+
+DROP TABLE sales_list;
+
+--
+-- Test: two specific errors for BY LIST partitioning:
+--   * new partitions do not have NULL value, which split partition has.
+--   * new partitions do not have a value that split partition has.
+--
+CREATE TABLE sales_list
+(salesperson_id INT,
+ salesperson_name VARCHAR(30),
+ sales_state VARCHAR(20),
+ sales_amount INT,
+ sales_date DATE)
+PARTITION BY LIST (sales_state);
+
+CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Helsinki', 'St. Petersburg', 'Oslo');
+CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Lisbon', 'New York', 'Madrid', 'Bejing', 'Berlin', 'Delhi', 'Kyiv', 'Vladivostok', NULL);
+
+-- ERROR:  new partitions do not have value NULL but split partition does
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'),
+   PARTITION sales_east FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'));
+
+-- ERROR:  new partitions do not have value 'Kyiv' but split partition does
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'),
+   PARTITION sales_east FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', NULL));
+
+DROP TABLE sales_list;
+
+--
+-- Test: BY LIST partitioning, SPLIT PARTITION with data
+--
+CREATE TABLE sales_list
+(salesperson_id SERIAL,
+ salesperson_name VARCHAR(30),
+ sales_state VARCHAR(20),
+ sales_amount INT,
+ sales_date DATE)
+PARTITION BY LIST (sales_state);
+
+CREATE INDEX sales_list_salesperson_name_idx ON sales_list USING btree (salesperson_name);
+CREATE INDEX sales_list_sales_state_idx ON sales_list USING btree (sales_state);
+
+CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Helsinki', 'St. Petersburg', 'Oslo');
+CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Lisbon', 'New York', 'Madrid', 'Bejing', 'Berlin', 'Delhi', 'Kyiv', 'Vladivostok');
+CREATE TABLE sales_others PARTITION OF sales_list DEFAULT;
+
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Trump',    'Bejing',         1000, '2022-03-01');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Smirnoff', 'New York',        500, '2022-03-03');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Ford',     'St. Petersburg', 2000, '2022-03-05');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Ivanov',   'Warsaw',          750, '2022-03-04');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Deev',     'Lisbon',          250, '2022-03-07');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Poirot',   'Berlin',         1000, '2022-03-01');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Oslo',           1200, '2022-03-06');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Li',       'Vladivostok',    1150, '2022-03-09');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Oslo',           1200, '2022-03-11');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Halder',   'Helsinki',        800, '2022-03-02');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Muller',   'Madrid',          650, '2022-03-05');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Smith',    'Kyiv',            350, '2022-03-10');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Gandi',    'Warsaw',          150, '2022-03-08');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES ('Plato',    'Lisbon',          950, '2022-03-05');
+
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'),
+   PARTITION sales_east FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'));
+
+SELECT * FROM sales_list;
+SELECT * FROM sales_west;
+SELECT * FROM sales_east;
+SELECT * FROM sales_nord;
+SELECT * FROM sales_central;
+
+-- Use indexscan for testing indexes after splitting partition
+SET enable_indexscan = ON;
+SET enable_seqscan = OFF;
+
+SELECT * FROM sales_central WHERE sales_state = 'Warsaw';
+SELECT * FROM sales_list WHERE sales_state = 'Warsaw';
+SELECT * FROM sales_list WHERE salesperson_name = 'Ivanov';
+
+SET enable_indexscan = ON;
+SET enable_seqscan = ON;
+
+DROP TABLE sales_list;
+
+--
+-- Test for:
+--   * split DEFAULT partition to partitions with spaces between bounds;
+--   * random order of partitions in SPLIT PARTITION command.
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-09');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
+INSERT INTO sales_range VALUES (4,  'Ivanov',   750,  '2022-04-13');
+INSERT INTO sales_range VALUES (5,  'Deev',     250,  '2022-04-07');
+INSERT INTO sales_range VALUES (6,  'Poirot',   150,  '2022-02-07');
+INSERT INTO sales_range VALUES (7,  'Li',       175,  '2022-03-08');
+INSERT INTO sales_range VALUES (8,  'Ericsson', 185,  '2022-02-23');
+INSERT INTO sales_range VALUES (9,  'Muller',   250,  '2022-03-11');
+INSERT INTO sales_range VALUES (10, 'Halder',   350,  '2022-01-28');
+INSERT INTO sales_range VALUES (11, 'Trump',    380,  '2022-04-06');
+INSERT INTO sales_range VALUES (12, 'Plato',    350,  '2022-03-19');
+INSERT INTO sales_range VALUES (13, 'Gandi',    377,  '2022-01-09');
+INSERT INTO sales_range VALUES (14, 'Smith',    510,  '2022-05-04');
+
+ALTER TABLE sales_range SPLIT PARTITION sales_others INTO
+  (PARTITION sales_others DEFAULT,
+   PARTITION sales_mar2022_1decade FOR VALUES FROM ('2022-03-01') TO ('2022-03-10'),
+   PARTITION sales_jan2022_1decade FOR VALUES FROM ('2022-01-01') TO ('2022-01-10'),
+   PARTITION sales_feb2022_1decade FOR VALUES FROM ('2022-02-01') TO ('2022-02-10'),
+   PARTITION sales_apr2022_1decade FOR VALUES FROM ('2022-04-01') TO ('2022-04-10'));
+
+SELECT * FROM sales_jan2022_1decade;
+SELECT * FROM sales_feb2022_1decade;
+SELECT * FROM sales_mar2022_1decade;
+SELECT * FROM sales_apr2022_1decade;
+SELECT * FROM sales_others;
+
+DROP TABLE sales_range;
+
+--
+-- Test for:
+--   * split non-DEFAULT partition to partitions with spaces between bounds;
+--   * random order of partitions in SPLIT PARTITION command.
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_all PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-05-01');
+CREATE TABLE sales_others PARTITION OF sales_range DEFAULT;
+
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-09');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
+INSERT INTO sales_range VALUES (4,  'Ivanov',   750,  '2022-04-13');
+INSERT INTO sales_range VALUES (5,  'Deev',     250,  '2022-04-07');
+INSERT INTO sales_range VALUES (6,  'Poirot',   150,  '2022-02-07');
+INSERT INTO sales_range VALUES (7,  'Li',       175,  '2022-03-08');
+INSERT INTO sales_range VALUES (8,  'Ericsson', 185,  '2022-02-23');
+INSERT INTO sales_range VALUES (9,  'Muller',   250,  '2022-03-11');
+INSERT INTO sales_range VALUES (10, 'Halder',   350,  '2022-01-28');
+INSERT INTO sales_range VALUES (11, 'Trump',    380,  '2022-04-06');
+INSERT INTO sales_range VALUES (12, 'Plato',    350,  '2022-03-19');
+INSERT INTO sales_range VALUES (13, 'Gandi',    377,  '2022-01-09');
+INSERT INTO sales_range VALUES (14, 'Smith',    510,  '2022-05-04');
+
+ALTER TABLE sales_range SPLIT PARTITION sales_all INTO
+  (PARTITION sales_mar2022_1decade FOR VALUES FROM ('2022-03-01') TO ('2022-03-10'),
+   PARTITION sales_jan2022_1decade FOR VALUES FROM ('2022-01-01') TO ('2022-01-10'),
+   PARTITION sales_feb2022_1decade FOR VALUES FROM ('2022-02-01') TO ('2022-02-10'),
+   PARTITION sales_apr2022_1decade FOR VALUES FROM ('2022-04-01') TO ('2022-04-10'));
+
+SELECT * FROM sales_jan2022_1decade;
+SELECT * FROM sales_feb2022_1decade;
+SELECT * FROM sales_mar2022_1decade;
+SELECT * FROM sales_apr2022_1decade;
+SELECT * FROM sales_others;
+
+DROP TABLE sales_range;
+
+--
+-- Test for split non-DEFAULT partition to DEFAULT partition + partitions
+-- with spaces between bounds.
+--
+CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
+CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01');
+CREATE TABLE sales_all PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01');
+
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-10');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
+INSERT INTO sales_range VALUES (4,  'Ivanov',   750,  '2022-04-13');
+INSERT INTO sales_range VALUES (5,  'Deev',     250,  '2022-04-07');
+INSERT INTO sales_range VALUES (6,  'Poirot',   150,  '2022-02-11');
+INSERT INTO sales_range VALUES (7,  'Li',       175,  '2022-03-08');
+INSERT INTO sales_range VALUES (8,  'Ericsson', 185,  '2022-02-23');
+INSERT INTO sales_range VALUES (9,  'Muller',   250,  '2022-03-11');
+INSERT INTO sales_range VALUES (10, 'Halder',   350,  '2022-01-28');
+INSERT INTO sales_range VALUES (11, 'Trump',    380,  '2022-04-06');
+INSERT INTO sales_range VALUES (12, 'Plato',    350,  '2022-03-19');
+INSERT INTO sales_range VALUES (13, 'Gandi',    377,  '2022-01-09');
+
+ALTER TABLE sales_range SPLIT PARTITION sales_all INTO
+  (PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'),
+   PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
+   PARTITION sales_others DEFAULT);
+
+INSERT INTO sales_range VALUES (14, 'Smith',    510,  '2022-05-04');
+
+SELECT * FROM sales_range;
+SELECT * FROM sales_jan2022;
+SELECT * FROM sales_feb2022;
+SELECT * FROM sales_apr2022;
+SELECT * FROM sales_others;
+
+DROP TABLE sales_range;
+
+--
+-- Try to SPLIT partition of another table.
+--
+CREATE TABLE t1(i int, t text) PARTITION BY LIST (t);
+CREATE TABLE t1pa PARTITION OF t1 FOR VALUES IN ('A');
+CREATE TABLE t2 (i int, t text) PARTITION BY RANGE (t);
+
+-- ERROR:  relation "t1pa" is not a partition of relation "t2"
+ALTER TABLE t2 SPLIT PARTITION t1pa INTO
+   (PARTITION t2a FOR VALUES FROM ('A') TO ('B'),
+    PARTITION t2b FOR VALUES FROM ('B') TO ('C'));
+
+DROP TABLE t2;
+DROP TABLE t1;
+
+--
+-- Try to SPLIT partition of temporary table.
+--
+CREATE TEMP TABLE t (i int) PARTITION BY RANGE (i);
+CREATE TEMP TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (2);
+
+SELECT c.oid::pg_catalog.regclass, pg_catalog.pg_get_expr(c.relpartbound, c.oid), c.relpersistence
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 't'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+
+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));
+
+-- Partitions should be temporary.
+SELECT c.oid::pg_catalog.regclass, pg_catalog.pg_get_expr(c.relpartbound, c.oid), c.relpersistence
+  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
+  WHERE c.oid = i.inhrelid AND i.inhparent = 't'::regclass
+  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;
+
+DROP TABLE t;
+
+-- Check the new partitions inherit parent's tablespace
+CREATE TABLE t (i int PRIMARY KEY USING INDEX TABLESPACE regress_tblspace)
+  PARTITION BY RANGE (i) TABLESPACE regress_tblspace;
+CREATE TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (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));
+SELECT tablename, tablespace FROM pg_tables
+  WHERE tablename IN ('t', 'tp_0_1', 'tp_1_2') AND schemaname = 'partition_split_schema'
+  ORDER BY tablename, tablespace;
+SELECT tablename, indexname, tablespace FROM pg_indexes
+  WHERE tablename IN ('t', 'tp_0_1', 'tp_1_2') AND schemaname = 'partition_split_schema'
+  ORDER BY tablename, indexname, tablespace;
+DROP TABLE t;
+
+-- Check new partitions inherits parent's table access method
+CREATE ACCESS METHOD partition_split_heap TYPE TABLE HANDLER heap_tableam_handler;
+CREATE TABLE t (i int) PARTITION BY RANGE (i) USING partition_split_heap;
+CREATE TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (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));
+SELECT c.relname, a.amname
+FROM pg_class c JOIN pg_am a ON c.relam = a.oid
+WHERE c.oid IN ('t'::regclass, 'tp_0_1'::regclass, 'tp_1_2'::regclass)
+ORDER BY c.relname;
+DROP TABLE t;
+DROP ACCESS METHOD partition_split_heap;
+
+-- Test permission checks.  The user needs to own the parent table and the
+-- the partition to split to do the split.
+CREATE ROLE regress_partition_split_alice;
+CREATE ROLE regress_partition_split_bob;
+GRANT ALL ON SCHEMA partition_split_schema TO regress_partition_split_alice;
+GRANT ALL ON SCHEMA partition_split_schema TO regress_partition_split_bob;
+
+SET SESSION AUTHORIZATION regress_partition_split_alice;
+CREATE TABLE t (i int) PARTITION BY RANGE (i);
+CREATE TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (2);
+
+SET SESSION AUTHORIZATION regress_partition_split_bob;
+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));
+RESET SESSION AUTHORIZATION;
+
+ALTER TABLE t OWNER TO regress_partition_split_bob;
+SET SESSION AUTHORIZATION regress_partition_split_bob;
+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));
+RESET SESSION AUTHORIZATION;
+
+ALTER TABLE tp_0_2 OWNER TO regress_partition_split_bob;
+SET SESSION AUTHORIZATION regress_partition_split_bob;
+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));
+RESET SESSION AUTHORIZATION;
+
+DROP TABLE t;
+REVOKE ALL ON SCHEMA partition_split_schema FROM regress_partition_split_alice;
+REVOKE ALL ON SCHEMA partition_split_schema FROM regress_partition_split_bob;
+DROP ROLE regress_partition_split_alice;
+DROP ROLE regress_partition_split_bob;
+
+-- Split partition of a temporary table when one of the partitions after
+-- split has the same name as the partition being split
+CREATE TEMP TABLE t (a int) PARTITION BY RANGE (a);
+CREATE TEMP TABLE tp_0 PARTITION OF t FOR VALUES FROM (0) TO (2);
+ALTER TABLE t SPLIT PARTITION tp_0 INTO
+  (PARTITION tp_0 FOR VALUES FROM (0) TO (1),
+   PARTITION tp_1 FOR VALUES FROM (1) TO (2));
+DROP TABLE t;
+
+RESET search_path;
+
+--
+DROP SCHEMA partition_split_schema;
+DROP SCHEMA partition_split_schema2;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ddc4c78fb9..16dcd62335 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2723,6 +2723,7 @@ SpecialJoinInfo
 SpinDelayStatus
 SplitInterval
 SplitLR
+SplitPartitionContext
 SplitPageLayout
 SplitPoint
 SplitTextOutputData
-- 
2.40.1.windows.1

---

From: Robert Haas <[email-masked]>
Date: Wed, 28 Aug 2024 09:45:36 -0400

> I didn't include fix 04158e7fa3 into patches because Robert Haas

To be clear, I'm not against 04158e7fa3. I just don't think it fixes everything.

> table OID (not name) and without using ProcessUtility().

Agree.

> new partitions now. (But might be in this case we should have an error

I think we want to avoid giving errors or warnings.  For all of these
cases, and others, we need to consider what the expected behavior is,
and have test cases and documentation as appropriate. But we shouldn't
think of it as "let's make it fail if the user does something that's
not safe" but rather "let's figure out how to make it safe."

> (Similar optimization was proposed in [3] but was rejected [4]).

Interesting. Maybe it would be a good idea to set up some test cases
to see which approach is better in different cases. Like try moving
data from foo1 to foo2 with DELETE..INSERT vs. creating a new table
with CTAS from foo1 UNION ALL foo2 and then indexing it. I think
Alexander has a good point there, but I think my point is good too so
I'm not sure which way wins.

---

From: Dmitry Koval <[email-masked]>
Date: Fri, 30 Aug 2024 11:43:10 +0300

Hi!

I plan to prepare fixes for issues from email [1] as separate commits 
(for better code readability). Attachment in this email is a variant of 
fix for the issue:

 > 1. Function createPartitionTable() should be rewritten using
 > partitioned table OID (not name) and without using ProcessUtility().

Patch "Refactor createPartitionTable to remove ProcessUtility call" 
contains code changes + test (see file 
v33-0003-Refactor-createPartitionTable-to-remove-ProcessU.patch).

But I'm not sure that refactoring createPartitionTable is the best 
solution. PostgreSQL code has issue CVE-2014-0062 (commit 5f17304) - see 
relation_openrv() call in expandTableLikeClause() function [2] (opening 
relation by name after we got relation Oid).
Example for reproduce relation_openrv() call:

CREATE TABLE t (b bigint, i int DEFAULT 100);
CREATE TABLE t1 (LIKE t_bigint INCLUDING ALL);

Commit 04158e7fa3 [3] (by Alexander Korotkov) might be a good fix for 
this issue. But if we keep commit 04158e7fa3, do we need to refactor the 
createPartitionTable function (for removing ProcessUtility)?
Perhaps the existing code
1) v33-0002-Implement-ALTER-TABLE-.-SPLIT-PARTITION-.-comman.patch
2) v33-0003-Refactor-createPartitionTable-to-remove-ProcessU.patch +
with patch 04158e7fa3 will look better.

I would be very grateful for comments and suggestions.

Links.
[1] 
https://www.postgresql.org/message-id/859476bf-3cb0-455e-b093-b8ab5ef17f0e%40postgrespro.ru
[2] 
https://github.com/postgres/postgres/blob/c39afc38cfec7c34b883095062a89a63b221521a/src/backend/parser/parse_utilcmd.c#L1171
[3] 
https://github.com/postgres/postgres/commit/04158e7fa37c2dda9c3421ca922d02807b86df19

-- 
With best regards,
Dmitry Koval

From dd414c86b18492ae97cf55164244af3bcdc00384 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:57:22 +0300
Subject: [PATCH v33 1/3] Implement ALTER TABLE ... MERGE PARTITIONS ...
 command

 

 
   <para>
 
+  <para>
 
  <refsect1>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b3cc6f8f69..1b845a6614 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -658,6 +658,8 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, const char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);

 /* ----------------------------------------------------------------
  *		DefineRelation
@@ -4677,6 +4679,10 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = AccessShareLock;
 				break;
 
+			case AT_MergePartitions:
+				cmd_lockmode = AccessExclusiveLock;
+				break;
+
 			default:			/* oops */
 				elog(ERROR, "unrecognized alter table type: %d",
 					 (int) cmd->subtype);
@@ -5074,6 +5080,11 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* No command-specific prep needed */
 			pass = AT_PASS_MISC;
 			break;
+		case AT_MergePartitions:
+			ATSimplePermissions(cmd->subtype, rel, ATT_TABLE);
+			/* No command-specific prep needed */
+			pass = AT_PASS_MISC;
+			break;
 		default:				/* oops */
 			elog(ERROR, "unrecognized alter table type: %d",
 				 (int) cmd->subtype);
@@ -5470,6 +5481,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 		case AT_DetachPartitionFinalize:
 			address = ATExecDetachPartitionFinalize(rel, ((PartitionCmd *) cmd->def)->name);
 			break;
+		case AT_MergePartitions:
+			cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode,
+									  cur_pass, context);
+			Assert(cmd != NULL);
+			Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
+			ATExecMergePartitions(wqueue, tab, rel, (PartitionCmd *) cmd->def,
+								  context);
+			break;
 		default:				/* oops */
 			elog(ERROR, "unrecognized alter table type: %d",
 				 (int) cmd->subtype);
@@ -6458,6 +6477,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
 			return "DETACH PARTITION";
 		case AT_DetachPartitionFinalize:
 			return "DETACH PARTITION ... FINALIZE";
+		case AT_MergePartitions:
+			return "MERGE PARTITIONS";
 		case AT_AddIdentity:
 			return "ALTER COLUMN ... ADD IDENTITY";
 		case AT_SetIdentity:
@@ -18280,6 +18301,37 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
 	}
 }
 
+/*
@@ -18482,23 +18534,8 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -20013,3 +20050,352 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*

 

+/*
+ * checkPartition

 				/*
 
 						/*

 
+/*

index 7a5a910562..79c9a023fd 100644

From 14135f0a073bf0e04ee8bf35c757735a10fb2d8a Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:58:09 +0300
Subject: [PATCH v33 2/3] Implement ALTER TABLE ... SPLIT PARTITION ... command

index 2770e81656..626d35514c 100644

 
 
+  <para>
index 1b845a6614..84eda40eb3 100644
@@ -658,6 +658,9 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);

@@ -4679,6 +4682,10 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = AccessShareLock;
 				break;
 
@@ -5080,6 +5087,11 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
@@ -5481,6 +5493,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
@@ -6477,6 +6497,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -20051,6 +20073,256 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 	return cstorage;
 }
 
+/*
@@ -20155,6 +20427,132 @@ createPartitionTable(RangeVar *newPartName, Relation modelRel,
 	return newRel;
 }
 
+/*

 
+partitions_list:
 
+/*

 
 /*
  * transformPartitionCmd

 
+/*
 
+/*
index b31be31321..f4ac473ca8 100644
@@ -13348,3 +13348,21 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+/*
 
 	/*

 
 
index 79c9a023fd..b0804691f5 100644

From 40820a0ee599c48643208ab5f49831f4a2415053 Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email-masked]>
Date: Thu, 29 Aug 2024 20:27:03 +0300
Subject: [PATCH v33 3/3] Refactor createPartitionTable to remove
 ProcessUtility call

---
 src/backend/catalog/heap.c                    |   8 +-
 src/backend/commands/tablecmds.c              | 278 ++++++++++++++----
 src/include/catalog/heap.h                    |   6 +
 src/test/regress/expected/partition_split.out |  59 ++++
 src/test/regress/sql/partition_split.sql      |  21 ++
 5 files changed, 314 insertions(+), 58 deletions(-)

diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 01b43cc6a8..7071343043 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -101,16 +101,12 @@ static ObjectAddress AddNewRelationType(const char *typeName,
 										Oid new_row_type,
 										Oid new_array_type);
 static void RelationRemoveInheritance(Oid relid);
-static Oid	StoreRelCheck(Relation rel, const char *ccname, Node *expr,
-						  bool is_validated, bool is_local, int inhcount,
-						  bool is_no_inherit, bool is_internal);
 static void StoreConstraints(Relation rel, List *cooked_constraints,
 							 bool is_internal);
 static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
 										bool allow_merge, bool is_local,
 										bool is_initially_valid,
 										bool is_no_inherit);
-static void SetRelationNumChecks(Relation rel, int numchecks);
 static Node *cookConstraint(ParseState *pstate,
 							Node *raw_constraint,
 							char *relname);
@@ -2070,7 +2066,7 @@ SetAttrMissing(Oid relid, char *attname, char *value)
  *
  * The OID of the new constraint is returned.
  */
-static Oid
+Oid
 StoreRelCheck(Relation rel, const char *ccname, Node *expr,
 			  bool is_validated, bool is_local, int inhcount,
 			  bool is_no_inherit, bool is_internal)
@@ -2657,7 +2653,7 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
  * relcache entries for the rel.  Also, this backend will rebuild its
  * own relcache entry at the next CommandCounterIncrement.
  */
-static void
+void
 SetRelationNumChecks(Relation rel, int numchecks)
 {
 	Relation	relrel;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 84eda40eb3..e2a42a228e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -20323,6 +20323,179 @@ moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPar
 	}
 }
 
+
+/*
+ * getAttributesList: return list of columns (ColumnDef) like model table
+ * (modelRel)
+ */
+static List *
+getAttributesList(Relation modelRel)
+{
+	AttrNumber	parent_attno;
+	TupleDesc	modelDesc;
+	List	   *colList = NIL;
+
+	modelDesc = RelationGetDescr(modelRel);
+
+	for (parent_attno = 1; parent_attno <= modelDesc->natts;
+		 parent_attno++)
+	{
+		Form_pg_attribute attribute = TupleDescAttr(modelDesc,
+													parent_attno - 1);
+		ColumnDef  *def;
+
+		/* Ignore dropped columns in the parent. */
+		if (attribute->attisdropped)
+			continue;
+
+		def = makeColumnDef(NameStr(attribute->attname), attribute->atttypid,
+							attribute->atttypmod, attribute->attcollation);
+
+		def->is_not_null = attribute->attnotnull;
+
+		/* Add to column list */
+		colList = lappend(colList, def);
+
+		/*
+		 * Although we don't transfer the column's default/generation
+		 * expression now, we need to mark it GENERATED if appropriate.
+		 */
+		if (attribute->atthasdef && attribute->attgenerated)
+			def->generated = attribute->attgenerated;
+
+		def->storage = attribute->attstorage;
+
+		/* Likewise, copy compression if requested */
+		if (CompressionMethodIsValid(attribute->attcompression))
+			def->compression =
+				pstrdup(GetCompressionMethodName(attribute->attcompression));
+		else
+			def->compression = NULL;
+	}
+
+	return colList;
+}
+
+
+/*
+ * createTableConstraints: create constraints, default values and generated
+ * values (prototype is function expandTableLikeClause).
+ */
+static void
+createTableConstraints(Relation modelRel, Relation newRel)
+{
+	TupleDesc	tupleDesc;
+	TupleConstr *constr;
+	AttrMap    *attmap;
+	AttrNumber	parent_attno;
+	int			ccnum;
+
+	tupleDesc = RelationGetDescr(modelRel);
+	constr = tupleDesc->constr;
+
+	if (!constr)
+		return;
+
+	/*
+	 * Construct a map from the LIKE relation's attnos to the child rel's.
+	 * This re-checks type match etc, although it shouldn't be possible to
+	 * have a failure since both tables are locked.
+	 */
+	attmap = build_attrmap_by_name(RelationGetDescr(newRel),
+								   tupleDesc,
+								   false);
+
+	/* Cycle for default values. */
+	for (parent_attno = 1; parent_attno <= tupleDesc->natts; parent_attno++)
+	{
+		Form_pg_attribute attribute = TupleDescAttr(tupleDesc,
+													parent_attno - 1);
+
+		/* Ignore dropped columns in the parent. */
+		if (attribute->attisdropped)
+			continue;
+
+		/* Copy default, if present and it should be copied. */
+		if (attribute->atthasdef)
+		{
+			Node	   *this_default = NULL;
+			AttrDefault *attrdef = constr->defval;
+			bool		found_whole_row;
+			int16		num;
+			Node	   *def;
+
+			/* Find default in constraint structure */
+			for (int i = 0; i < constr->num_defval; i++)
+			{
+				if (attrdef[i].adnum == parent_attno)
+				{
+					this_default = stringToNode(attrdef[i].adbin);
+					break;
+				}
+			}
+			if (this_default == NULL)
+				elog(ERROR, "default expression not found for attribute %d of relation \"%s\"",
+					 parent_attno, RelationGetRelationName(modelRel));
+
+			num = attmap->attnums[parent_attno - 1];
+			def = map_variable_attnos(this_default, 1, 0, attmap, InvalidOid, &found_whole_row);
+
+			/*
+			 * Prevent this for the same reason as for constraints below. Note
+			 * that defaults cannot contain any vars, so it's OK that the
+			 * error message refers to generated columns.
+			 */
+			if (found_whole_row)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cannot convert whole-row table reference"),
+						 errdetail("Generation expression for column \"%s\" contains a whole-row reference to table \"%s\".",
+								   NameStr(attribute->attname),
+								   RelationGetRelationName(modelRel))));
+
+			/* Add a pre-cooked default expression. */
+			(void) StoreAttrDefault(newRel, num, def, true, false);
+		}
+	}
+
+	/* Cycle for CHECK constraints. */
+	for (ccnum = 0; ccnum < constr->num_check; ccnum++)
+	{
+		char	   *ccname = constr->check[ccnum].ccname;
+		char	   *ccbin = constr->check[ccnum].ccbin;
+		bool		ccnoinherit = constr->check[ccnum].ccnoinherit;
+		Node	   *ccbin_node;
+		bool		found_whole_row;
+
+		ccbin_node = map_variable_attnos(stringToNode(ccbin),
+										 1, 0,
+										 attmap,
+										 InvalidOid, &found_whole_row);
+
+		/*
+		 * We reject whole-row variables because the whole point of LIKE is
+		 * that the new table's rowtype might later diverge from the parent's.
+		 * So, while translation might be possible right now, it wouldn't be
+		 * possible to guarantee it would work in future.
+		 */
+		if (found_whole_row)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("cannot convert whole-row table reference"),
+					 errdetail("Constraint \"%s\" contains a whole-row reference to table \"%s\".",
+							   ccname,
+							   RelationGetRelationName(modelRel))));
+
+		/* We can skip validation, since the new table should be empty. */
+		(void) StoreRelCheck(newRel, ccname, ccbin_node, true, true,
+							 0, ccnoinherit, false);
+	}
+
+	/* Update the count of constraints in the relation's pg_class tuple. */
+	SetRelationNumChecks(newRel, constr->num_check);
+}
+
+
 /*
  * createPartitionTable: create table for a new partition with given name
  * (newPartName) like table (modelRel)
@@ -20337,13 +20510,14 @@ moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPar
  * Function returns the created relation (locked in AccessExclusiveLock mode).
  */
 static Relation
-createPartitionTable(RangeVar *newPartName, Relation modelRel,
-					 AlterTableUtilityContext *context)
+createPartitionTable(RangeVar *newPartName, Relation modelRel)
 {
-	CreateStmt *createStmt;
-	TableLikeClause *tlc;
-	PlannedStmt *wrapper;
 	Relation	newRel;
+	Oid			newRelId;
+	TupleDesc	descriptor;
+	List	   *colList = NIL;
+	Oid			relamId;
+	Oid			namespaceId;
 
 	/* If existing rel is temp, it must belong to this session */
 	if (modelRel->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
@@ -20352,56 +20526,53 @@ createPartitionTable(RangeVar *newPartName, Relation modelRel,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 				 errmsg("cannot create as partition of temporary relation of another session")));
 
-	/* New partition should have the same persistence as modelRel */
-	newPartName->relpersistence = modelRel->rd_rel->relpersistence;
-
-	createStmt = makeNode(CreateStmt);
-	createStmt->relation = newPartName;
-	createStmt->tableElts = NIL;
-	createStmt->inhRelations = NIL;
-	createStmt->constraints = NIL;
-	createStmt->options = NIL;
-	createStmt->oncommit = ONCOMMIT_NOOP;
-	createStmt->tablespacename = get_tablespace_name(modelRel->rd_rel->reltablespace);
-	createStmt->if_not_exists = false;
-	createStmt->accessMethod = get_am_name(modelRel->rd_rel->relam);
-
-	tlc = makeNode(TableLikeClause);
-	tlc->relation = makeRangeVar(get_namespace_name(RelationGetNamespace(modelRel)),
-								 RelationGetRelationName(modelRel), -1);
-
-	/*
-	 * Indexes will be inherited on "attach new partitions" stage, after data
-	 * moving.  We also don't copy the extended statistics for consistency
-	 * with CREATE TABLE PARTITION OF.
-	 */
-	tlc->options = CREATE_TABLE_LIKE_ALL &
-		~(CREATE_TABLE_LIKE_INDEXES | CREATE_TABLE_LIKE_IDENTITY | CREATE_TABLE_LIKE_STATISTICS);
-	tlc->relationOid = InvalidOid;
-	createStmt->tableElts = lappend(createStmt->tableElts, tlc);
-
-	/* Need to make a wrapper PlannedStmt. */
-	wrapper = makeNode(PlannedStmt);
-	wrapper->commandType = CMD_UTILITY;
-	wrapper->canSetTag = false;
-	wrapper->utilityStmt = (Node *) createStmt;
-	wrapper->stmt_location = context->pstmt->stmt_location;
-	wrapper->stmt_len = context->pstmt->stmt_len;
-
-	ProcessUtility(wrapper,
-				   context->queryString,
-				   false,
-				   PROCESS_UTILITY_SUBCOMMAND,
-				   NULL,
-				   NULL,
-				   None_Receiver,
-				   NULL);
+	/* Look up inheritance ancestors and generate relation schema. */
+	colList = getAttributesList(modelRel);
+
+	/* Create a tuple descriptor from the relation schema. */
+	descriptor = BuildDescForRelation(colList);
+
+	/* Look up the access method for new relation. */
+	relamId = (modelRel->rd_rel->relam != InvalidOid) ? modelRel->rd_rel->relam : HEAP_TABLE_AM_OID;
+
+	/* Look up the namespace in which we are supposed to create the relation. */
+	namespaceId =
+		RangeVarGetAndCheckCreationNamespace(newPartName, NoLock, NULL);
+
+	/* Create the relation. */
+	newRelId = heap_create_with_catalog(newPartName->relname,
+										namespaceId,
+										modelRel->rd_rel->reltablespace,
+										InvalidOid,
+										InvalidOid,
+										InvalidOid,
+										GetUserId(),
+										relamId,
+										descriptor,
+										NIL,
+										RELKIND_RELATION,
+										newPartName->relpersistence,
+										false,
+										false,
+										ONCOMMIT_NOOP,
+										(Datum) 0,
+										true,
+										allowSystemTableMods,
+										false,
+										InvalidOid,
+										NULL);
+
+	/*
+	 * We must bump the command counter to make the newly-created relation
+	 * tuple visible for opening.
+	 */
+	CommandCounterIncrement();
 
 	/*
 	 * Open the new partition with no lock, because we already have
 	 * AccessExclusiveLock placed there after creation.
 	 */
-	newRel = table_openrv(newPartName, NoLock);
+	newRel = table_open(newRelId, NoLock);
 
 	/*
 	 * We intended to create the partition with the same persistence as the
@@ -20424,6 +20595,9 @@ createPartitionTable(RangeVar *newPartName, Relation modelRel,
 				 errmsg("cannot create a permanent relation as partition of temporary relation \"%s\"",
 						RelationGetRelationName(modelRel))));
 
+	/* Create constraints, default values and generated values */
+	createTableConstraints(modelRel, newRel);
+
 	return newRel;
 }
 
@@ -20521,7 +20695,7 @@ ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
 		Relation	newPartRel;
 
-		newPartRel = createPartitionTable(sps->name, rel, context);
+		newPartRel = createPartitionTable(sps->name, rel);
 		newPartRels = lappend(newPartRels, newPartRel);
 	}
 
@@ -20765,7 +20939,7 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	}
 
 	/* Create table for new partition, use partitioned table as model. */
-	newPartRel = createPartitionTable(cmd->name, rel, context);
+	newPartRel = createPartitionTable(cmd->name, rel);
 
 	/* Copy data from merged partitions to new partition. */
 	moveMergedTablesRows(rel, mergingPartitionsList, newPartRel);
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index c512824cd1..9fd52a4f39 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -114,6 +114,12 @@ extern List *AddRelationNewConstraints(Relation rel,
 									   bool is_internal,
 									   const char *queryString);
 
+extern Oid StoreRelCheck(Relation rel, const char *ccname, Node *expr,
+						 bool is_validated, bool is_local, int inhcount,
+						 bool is_no_inherit, bool is_internal);
+
+extern void SetRelationNumChecks(Relation rel, int numchecks);
+
 extern void RelationClearMissing(Relation rel);
 extern void SetAttrMissing(Oid relid, char *attname, char *value);
 
diff --git a/src/test/regress/expected/partition_split.out b/src/test/regress/expected/partition_split.out
index dc9a5130cc..205ddb0f77 100644
--- a/src/test/regress/expected/partition_split.out
+++ b/src/test/regress/expected/partition_split.out
@@ -1583,6 +1583,65 @@ ALTER TABLE t SPLIT PARTITION tp_0 INTO
   (PARTITION tp_0 FOR VALUES FROM (0) TO (1),
    PARTITION tp_1 FOR VALUES FROM (1) TO (2));
 DROP TABLE t;
+-- Check defaults and constraints of new partitions
+CREATE TABLE t_bigint (
+  b bigint,
+  i int DEFAULT (3+10),
+  j int DEFAULT 101,
+  k int GENERATED ALWAYS AS (b+10) STORED
+)
+PARTITION BY RANGE (b);
+CREATE TABLE t_bigint_default PARTITION OF t_bigint DEFAULT;
+-- Show defaults/constraints before SPLIT PARTITION
+\d+ t_bigint
+                                  Partitioned table "partition_split_schema.t_bigint"
+ Column |  Type   | Collation | Nullable |                Default                | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------------------------------------+---------+--------------+-------------
+ b      | bigint  |           |          |                                       | plain   |              | 
+ i      | integer |           |          | 3 + 10                                | plain   |              | 
+ j      | integer |           |          | 101                                   | plain   |              | 
+ k      | integer |           |          | generated always as ((b + 10)) stored | plain   |              | 
+Partition key: RANGE (b)
+Partitions: t_bigint_default DEFAULT
+
+\d+ t_bigint_default
+                                    Table "partition_split_schema.t_bigint_default"
+ Column |  Type   | Collation | Nullable |                Default                | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------------------------------------+---------+--------------+-------------
+ b      | bigint  |           |          |                                       | plain   |              | 
+ i      | integer |           |          | 3 + 10                                | plain   |              | 
+ j      | integer |           |          | 101                                   | plain   |              | 
+ k      | integer |           |          | generated always as ((b + 10)) stored | plain   |              | 
+Partition of: t_bigint DEFAULT
+No partition constraint
+
+ALTER TABLE t_bigint SPLIT PARTITION t_bigint_default INTO
+  (PARTITION t_bigint_01_10 FOR VALUES FROM (0) TO (10),
+   PARTITION t_bigint_default DEFAULT);
+-- Show defaults/constraints after SPLIT PARTITION
+\d+ t_bigint_default
+                                    Table "partition_split_schema.t_bigint_default"
+ Column |  Type   | Collation | Nullable |                Default                | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------------------------------------+---------+--------------+-------------
+ b      | bigint  |           |          |                                       | plain   |              | 
+ i      | integer |           |          | 3 + 10                                | plain   |              | 
+ j      | integer |           |          | 101                                   | plain   |              | 
+ k      | integer |           |          | generated always as ((b + 10)) stored | plain   |              | 
+Partition of: t_bigint DEFAULT
+Partition constraint: (NOT ((b IS NOT NULL) AND ((b >= '0'::bigint) AND (b < '10'::bigint))))
+
+\d+ t_bigint_01_10
+                                     Table "partition_split_schema.t_bigint_01_10"
+ Column |  Type   | Collation | Nullable |                Default                | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------------------------------------+---------+--------------+-------------
+ b      | bigint  |           |          |                                       | plain   |              | 
+ i      | integer |           |          | 3 + 10                                | plain   |              | 
+ j      | integer |           |          | 101                                   | plain   |              | 
+ k      | integer |           |          | generated always as ((b + 10)) stored | plain   |              | 
+Partition of: t_bigint FOR VALUES FROM ('0') TO ('10')
+Partition constraint: ((b IS NOT NULL) AND (b >= '0'::bigint) AND (b < '10'::bigint))
+
+DROP TABLE t_bigint;
 RESET search_path;
 --
 DROP SCHEMA partition_split_schema;
diff --git a/src/test/regress/sql/partition_split.sql b/src/test/regress/sql/partition_split.sql
index ef5ea07f74..e185458e4e 100644
--- a/src/test/regress/sql/partition_split.sql
+++ b/src/test/regress/sql/partition_split.sql
@@ -955,6 +955,27 @@ ALTER TABLE t SPLIT PARTITION tp_0 INTO
    PARTITION tp_1 FOR VALUES FROM (1) TO (2));
 DROP TABLE t;
 
+-- Check defaults and constraints of new partitions
+CREATE TABLE t_bigint (
+  b bigint,
+  i int DEFAULT (3+10),
+  j int DEFAULT 101,
+  k int GENERATED ALWAYS AS (b+10) STORED
+)
+PARTITION BY RANGE (b);
+CREATE TABLE t_bigint_default PARTITION OF t_bigint DEFAULT;
+-- Show defaults/constraints before SPLIT PARTITION
+\d+ t_bigint
+\d+ t_bigint_default
+ALTER TABLE t_bigint SPLIT PARTITION t_bigint_default INTO
+  (PARTITION t_bigint_01_10 FOR VALUES FROM (0) TO (10),
+   PARTITION t_bigint_default DEFAULT);
+-- Show defaults/constraints after SPLIT PARTITION
+\d+ t_bigint_default
+\d+ t_bigint_01_10
+DROP TABLE t_bigint;
+
+
 RESET search_path;
 
 --
-- 
2.40.1.windows.1

---

From: Alexander Korotkov <[email-masked]>
Date: Mon, 9 Dec 2024 08:36:25 +0200

Hi!

> I would be very grateful for comments and suggestions.

Thank you for continuing your work on the subject.  The patches
currently doesn't apply cleanly.  Please, rebase.

I think getting away from expandTableLikeClause() is the right
direction to resolve the security problems.  That looks great, it
finally not as complex as I thought.  I think the code requires some
polishing: you need to revise the comments given its not part of LIKE
clause handling anymore.

I see fixes for the issues mentioned in [1] and [2] are still not
implemented.  Do you plan to do this in this release cycle?

Links.
1. https://www.postgresql.org/message-id/CA%2BTgmoY0%3DbT_xBP8csR%3DMFE%3DFxGE2n2-me2-31jBOgEcLvW7ug%40mail.gmail.com
2. https://www.postgresql.org/message-id/859476bf-3cb0-455e-b093-b8ab5ef17f0e%40postgrespro.ru

------
Regards,
Alexander Korotkov
Supabase

---

From: Dmitry Koval <[email-masked]>
Date: Tue, 10 Dec 2024 00:01:43 +0300

Thank you for email!

 >The patches currently doesn't apply cleanly.  Please, rebase.

I did rebase and made few fixes that were needed after the recent 
vanilla changes (added ATT_PARTITIONED_TABLE flag for 
ATSimplePermissions function + added creation of not-null constraints in 
the createTableConstraints function).
The patches are attached to the email.

 >I think the code requires some polishing: you need to revise
 > the comments given its not part of LIKE clause handling anymore.

I removed couple of lines from comment and a few lines from 
documentation, I hope that's enough ...

 >implemented.  Do you plan to do this in this release cycle?

I would like to make some changes, but I think it would be appropriate 
to discuss these points first.
As far as I understand, there is currently no clear opinion on how to 
implement [1] and [2].

I would appreciate your opinions on what improvements are really needed 
and in what order they should be implemented.

-- 
With best regards,
Dmitry Koval

Postgres Professional: http://postgrespro.com
From f088f654411516e2d924c07c61296944fc89cdc2 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:57:22 +0300
Subject: [PATCH v34 1/3] Implement ALTER TABLE ... MERGE PARTITIONS ...
 command

Fixes (summary information).
Authors: Alexander Korotkov, Tender Wang, Richard Guo, Dagfinn Ilmari Mannsaker
Authors: Fujii Masao
Reviewed-by: Alexander Korotkov, Robert Haas, Justin Pryzby, Pavel Borisov
Reviewed-by: Masahiko Sawada
Reported-by: Alexander Lakhin, Justin Pryzby, Kyotaro Horiguchi
Reported-by: Daniel Gustafsson, Tom Lane, Noah Misch
---
 doc/src/sgml/ddl.sgml                         |  19 +
 doc/src/sgml/ref/alter_table.sgml             |  88 +-
 src/backend/commands/tablecmds.c              | 417 +++++++-
 src/backend/parser/gram.y                     |  22 +-
 src/backend/parser/parse_utilcmd.c            | 126 +++
 src/backend/partitioning/partbounds.c         | 212 +++-
 src/bin/psql/tab-complete.in.c                |  10 +
 src/include/nodes/parsenodes.h                |  14 +
 src/include/parser/kwlist.h                   |   1 +
 src/include/partitioning/partbounds.h         |   6 +
 .../isolation/expected/partition-merge.out    | 199 ++++
 src/test/isolation/isolation_schedule         |   1 +
 src/test/isolation/specs/partition-merge.spec |  54 +
 .../test_ddl_deparse/test_ddl_deparse.c       |   3 +
 src/test/regress/expected/partition_merge.out | 947 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/partition_merge.sql      | 609 +++++++++++
 src/tools/pgindent/typedefs.list              |   1 +
 18 files changed, 2707 insertions(+), 24 deletions(-)
 create mode 100644 src/test/isolation/expected/partition-merge.out
 create mode 100644 src/test/isolation/specs/partition-merge.spec
 create mode 100644 src/test/regress/expected/partition_merge.out
 create mode 100644 src/test/regress/sql/partition_merge.sql

index dea04d64db..e6ad4144f8 100644
@@ -4417,6 +4417,25 @@ ALTER TABLE measurement_y2006m02 ADD UNIQUE (city_id, logdate);
index c8f7ab7d95..690e8e99c5 100644

@@ -1119,14 +1122,85 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>

   <para>
    All the forms of <command>ALTER TABLE</command> that act on a single table,
    except <literal>RENAME</literal>, <literal>SET SCHEMA</literal>,
-   <literal>ATTACH PARTITION</literal>, and
-   <literal>DETACH PARTITION</literal> can be combined into
+   <literal>ATTACH PARTITION</literal>, <literal>DETACH PARTITION</literal>,
+   and <literal>MERGE PARTITIONS</literal> can be combined into
    a list of multiple alterations to be applied together.  For example, it
    is possible to add several columns and/or alter the type of several
    columns in a single command.  This is particularly useful with large
@@ -1369,7 +1443,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       <term><replaceable class="parameter">partition_name</replaceable></term>
       <listitem>
        <para>
-        The name of the table to attach as a new partition or to detach from this table.
+        The name of the table to attach as a new partition or to detach from this table,
+        or the name of the new merged partition.
        </para>
       </listitem>
      </varlistentry>
@@ -1785,6 +1860,13 @@ ALTER TABLE measurement
     DETACH PARTITION measurement_y2015m12;
 </programlisting></para>
 
+  <para>
 
  <refsect1>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6ccae4cb4a..34f7aa6130 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -689,6 +689,8 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, const char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);

 /* ----------------------------------------------------------------
  *		DefineRelation
@@ -4757,6 +4759,10 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = ShareUpdateExclusiveLock;
 				break;
 
+			case AT_MergePartitions:
+				cmd_lockmode = AccessExclusiveLock;
+				break;
+
 			default:			/* oops */
 				elog(ERROR, "unrecognized alter table type: %d",
 					 (int) cmd->subtype);
@@ -5190,6 +5196,11 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* No command-specific prep needed */
 			pass = AT_PASS_MISC;
 			break;
+		case AT_MergePartitions:
+			ATSimplePermissions(cmd->subtype, rel, ATT_PARTITIONED_TABLE);
+			/* No command-specific prep needed */
+			pass = AT_PASS_MISC;
+			break;
 		default:				/* oops */
 			elog(ERROR, "unrecognized alter table type: %d",
 				 (int) cmd->subtype);
@@ -5584,6 +5595,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 		case AT_DetachPartitionFinalize:
 			address = ATExecDetachPartitionFinalize(rel, ((PartitionCmd *) cmd->def)->name);
 			break;
+		case AT_MergePartitions:
+			cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode,
+									  cur_pass, context);
+			Assert(cmd != NULL);
+			Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
+			ATExecMergePartitions(wqueue, tab, rel, (PartitionCmd *) cmd->def,
+								  context);
+			break;
 		default:				/* oops */
 			elog(ERROR, "unrecognized alter table type: %d",
 				 (int) cmd->subtype);
@@ -6563,6 +6582,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
 			return "DETACH PARTITION";
 		case AT_DetachPartitionFinalize:
 			return "DETACH PARTITION ... FINALIZE";
+		case AT_MergePartitions:
+			return "MERGE PARTITIONS";
 		case AT_AddIdentity:
 			return "ALTER COLUMN ... ADD IDENTITY";
 		case AT_SetIdentity:
@@ -18980,6 +19001,37 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
 	}
 }
 
+/*
@@ -19183,23 +19235,8 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -20808,3 +20845,349 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
index 67eb96396a..713aac246e 100644
@@ -748,7 +748,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
-	PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH
+	PARALLEL PARAMETER PARSER PARTIAL PARTITION PARTITIONS PASSING PASSWORD PATH
 	PERIOD PLACING PLAN PLANS POLICY
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
 	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
@@ -2324,6 +2324,7 @@ partition_cmd:
 					n->subtype = AT_AttachPartition;
 					cmd->name = $3;
 					cmd->bound = $4;
+					cmd->partlist = NULL;
 					cmd->concurrent = false;
 					n->def = (Node *) cmd;
 
@@ -2338,6 +2339,7 @@ partition_cmd:
 					n->subtype = AT_DetachPartition;
 					cmd->name = $3;
 					cmd->bound = NULL;
+					cmd->partlist = NULL;
 					cmd->concurrent = $4;
 					n->def = (Node *) cmd;
 
@@ -2351,6 +2353,21 @@ partition_cmd:
@@ -2367,6 +2384,7 @@ index_partition_cmd:
 
@@ -17802,6 +17820,7 @@ unreserved_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PATH
@@ -18426,6 +18445,7 @@ bare_label_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PATH
index 0f324ee4e3..4c670765a6 100644
@@ -3476,6 +3479,116 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
 }

+/*
+ * checkPartition
@@ -3753,6 +3866,19 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,

 				/*
 
 						/*
 
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index bbd08770c3..8f52056fe8 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2685,6 +2685,7 @@ match_previous_words(int pattern_id,
@@ -2951,6 +2952,15 @@ match_previous_words(int pattern_id,
 
index 0f9462493e..470082e12a 100644
@@ -939,6 +939,17 @@ typedef struct PartitionRangeDatum
 
+/*
@@ -947,6 +958,8 @@ typedef struct PartitionCmd
 
@@ -2419,6 +2432,7 @@ typedef enum AlterTableType
index 899d64ad55..1580d863f2 100644
 
index 98d237ac68..70f45b3c60 100644
@@ -296,6 +296,9 @@ get_altertable_subcmdinfo(PG_FUNCTION_ARGS)
index 0000000000..7a960fed8f
@@ -0,0 +1,947 @@
+Not-null constraints:
+    "t_i_not_null" NOT NULL "i" (inherited)
index 81e4222d26..ff55527ec6 100644
 
index ce33e55bf1..6e55061044 100644
@@ -2666,6 +2666,7 @@ SimpleStats
 SimpleStringList
 SimpleStringListCell
 SingleBoundSortItem
+SinglePartitionSpec
 Size
 SkipPages
 SlabBlock
-- 
2.40.1.windows.1

From 2519a010e9ebd4dd5f9aed57a8dab6c606ee70c0 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:58:09 +0300
Subject: [PATCH v34 2/3] Implement ALTER TABLE ... SPLIT PARTITION ... command

 doc/src/sgml/ref/alter_table.sgml             |   77 +-
 src/backend/parser/parse_utilcmd.c            |   59 +-
 src/bin/psql/tab-complete.in.c                |   10 +-
 src/test/regress/expected/partition_split.out | 1590 +++++++++++++++++
 20 files changed, 4103 insertions(+), 17 deletions(-)

index e6ad4144f8..a6a91293f7 100644
@@ -4438,6 +4438,25 @@ ALTER TABLE measurement
 
index 690e8e99c5..dd31e92bfb 100644
@@ -1122,6 +1126,56 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>

@@ -1200,7 +1254,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
    All the forms of <command>ALTER TABLE</command> that act on a single table,
    except <literal>RENAME</literal>, <literal>SET SCHEMA</literal>,
    <literal>ATTACH PARTITION</literal>, <literal>DETACH PARTITION</literal>,
-   and <literal>MERGE PARTITIONS</literal> can be combined into
+   <literal>SPLIT PARTITION</literal>, and <literal>MERGE PARTITIONS</literal>
+   can be combined into
    a list of multiple alterations to be applied together.  For example, it
    is possible to add several columns and/or alter the type of several
    columns in a single command.  This is particularly useful with large
@@ -1444,7 +1499,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       <listitem>
        <para>
         The name of the table to attach as a new partition or to detach from this table,
-        or the name of the new merged partition.
+        or the name of split partition, or the name of the new merged partition.
        </para>
       </listitem>
      </varlistentry>
@@ -1860,6 +1915,24 @@ ALTER TABLE measurement
     DETACH PARTITION measurement_y2015m12;
 </programlisting></para>
 
+  <para>
index 34f7aa6130..93d46d85b8 100644
@@ -689,6 +689,9 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);

@@ -4759,6 +4762,10 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = ShareUpdateExclusiveLock;
 				break;
 
+			case AT_SplitPartition:
+				cmd_lockmode = AccessExclusiveLock;
+				break;
+
 			case AT_MergePartitions:
 				cmd_lockmode = AccessExclusiveLock;
 				break;
@@ -5196,6 +5203,11 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* No command-specific prep needed */
 			pass = AT_PASS_MISC;
 			break;
+		case AT_SplitPartition:
+			ATSimplePermissions(cmd->subtype, rel, ATT_PARTITIONED_TABLE);
+			/* No command-specific prep needed */
+			pass = AT_PASS_MISC;
+			break;
 		case AT_MergePartitions:
 			ATSimplePermissions(cmd->subtype, rel, ATT_PARTITIONED_TABLE);
 			/* No command-specific prep needed */
@@ -5595,6 +5607,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 		case AT_DetachPartitionFinalize:
 			address = ATExecDetachPartitionFinalize(rel, ((PartitionCmd *) cmd->def)->name);
 			break;
+		case AT_SplitPartition:
+			cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode,
+									  cur_pass, context);
+			Assert(cmd != NULL);
+			Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
+			ATExecSplitPartition(wqueue, tab, rel, (PartitionCmd *) cmd->def,
+								 context);
+			break;
 		case AT_MergePartitions:
 			cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode,
 									  cur_pass, context);
@@ -6582,6 +6602,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
 			return "DETACH PARTITION";
 		case AT_DetachPartitionFinalize:
 			return "DETACH PARTITION ... FINALIZE";
+		case AT_SplitPartition:
+			return "SPLIT PARTITION";
 		case AT_MergePartitions:
 			return "MERGE PARTITIONS";
 		case AT_AddIdentity:
@@ -20846,6 +20868,256 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 	return cstorage;
 }
 
+/*
@@ -20947,6 +21219,132 @@ createPartitionTable(RangeVar *newPartName, Relation modelRel,
 	return newRel;
 }
 
+/*
index 713aac246e..52ca864b7a 100644
@@ -256,6 +256,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -633,6 +634,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -763,7 +766,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -2314,6 +2317,23 @@ alter_table_cmds:
 			| alter_table_cmds ',' alter_table_cmd	{ $$ = lappend($1, $3); }
 		;
 
+partitions_list:
@@ -2358,6 +2378,20 @@ partition_cmd:
@@ -17889,6 +17923,7 @@ unreserved_keyword:
@@ -18526,6 +18561,7 @@ bare_label_keyword:
index 4c670765a6..bf5feee4c1 100644
@@ -137,7 +137,7 @@ static void transformConstraintAttrs(CreateStmtContext *cxt,
@@ -3517,6 +3517,40 @@ checkPartition(Relation rel, Oid partRelOid)
 
+/*
@@ -3858,7 +3892,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3866,6 +3900,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3874,7 +3909,11 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
@@ -4269,13 +4308,13 @@ setSchemaName(const char *context_schema, char **stmt_schema_name)
 
 /*
  * transformPartitionCmd
 
@@ -4284,9 +4323,9 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 
@@ -4294,7 +4333,7 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 
+/*
 
+/*
index 2194ab3dfa..f9db87be4f 100644
@@ -13546,3 +13546,21 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+/*
+ * get_list_partvalue_string
+ *		A C string representation of one list partition value
+ */
+char *
+get_list_partvalue_string(Const *val)
+{
+	deparse_context context;
+	StringInfo	buf = makeStringInfo();
+
+	memset(&context, 0, sizeof(deparse_context));
+	context.buf = buf;
+
+	get_const_expr(val, &context, -1);
+
+	return buf->data;
+}
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 8f52056fe8..250a983f41 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2685,7 +2685,7 @@ match_previous_words(int pattern_id,
 					  "OWNER TO", "SET", "VALIDATE CONSTRAINT",
 					  "REPLICA IDENTITY", "ATTACH PARTITION",
 					  "DETACH PARTITION", "FORCE ROW LEVEL SECURITY",
-					  "MERGE PARTITIONS (",
+					  "SPLIT PARTITION", "MERGE PARTITIONS (",
 					  "OF", "NOT OF");
 	/* ALTER TABLE xxx ADD */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ADD"))
@@ -2941,10 +2941,10 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("FROM (", "IN (", "WITH (");
 
 	/*
@@ -2952,6 +2952,10 @@ match_previous_words(int pattern_id,
 
index 470082e12a..58972b22c6 100644
@@ -2432,6 +2432,7 @@ typedef enum AlterTableType
index 1580d863f2..a4b4b330ce 100644
@@ -419,6 +419,7 @@ PG_KEYWORD("smallint", SMALLINT, COL_NAME_KEYWORD, BARE_LABEL)

 
index 70f45b3c60..49cd292a0c 100644
@@ -296,6 +296,9 @@ get_altertable_subcmdinfo(PG_FUNCTION_ARGS)
index 0000000000..0128f53d93
@@ -0,0 +1,1590 @@
+ERROR:  ALTER action SPLIT PARTITION cannot be performed on relation "sales_feb_mar_apr2022"
+DETAIL:  This operation is not supported for tables.
index ff55527ec6..24ab52afce 100644
 
index 6e55061044..f662ed0c41 100644
@@ -2730,6 +2730,7 @@ SpecialJoinInfo
 SpinDelayStatus
 SplitInterval
 SplitLR
+SplitPartitionContext
 SplitPageLayout
 SplitPoint
 SplitTextOutputData
-- 
2.40.1.windows.1

From c5d88bf6ec53ea562736c62b71ab598159f6384a Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email-masked]>
Date: Thu, 29 Aug 2024 20:27:03 +0300
Subject: [PATCH v34 3/3] Refactor createPartitionTable to remove
 ProcessUtility call

---
 src/backend/catalog/heap.c                    |   8 +-
 src/backend/commands/tablecmds.c              | 290 ++++++++++++++----
 src/include/catalog/heap.h                    |   6 +
 src/test/regress/expected/partition_split.out |  59 ++++
 src/test/regress/sql/partition_split.sql      |  21 ++
 5 files changed, 326 insertions(+), 58 deletions(-)

diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index d7b88b61dc..5e8078baf2 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -101,16 +101,12 @@ static ObjectAddress AddNewRelationType(const char *typeName,
 										Oid new_row_type,
 										Oid new_array_type);
 static void RelationRemoveInheritance(Oid relid);
-static Oid	StoreRelCheck(Relation rel, const char *ccname, Node *expr,
-						  bool is_validated, bool is_local, int16 inhcount,
-						  bool is_no_inherit, bool is_internal);
 static void StoreConstraints(Relation rel, List *cooked_constraints,
 							 bool is_internal);
 static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
 										bool allow_merge, bool is_local,
 										bool is_initially_valid,
 										bool is_no_inherit);
-static void SetRelationNumChecks(Relation rel, int numchecks);
 static Node *cookConstraint(ParseState *pstate,
 							Node *raw_constraint,
 							char *relname);
@@ -2072,7 +2068,7 @@ SetAttrMissing(Oid relid, char *attname, char *value)
  *
  * The OID of the new constraint is returned.
  */
-static Oid
+Oid
 StoreRelCheck(Relation rel, const char *ccname, Node *expr,
 			  bool is_validated, bool is_local, int16 inhcount,
 			  bool is_no_inherit, bool is_internal)
@@ -3045,7 +3041,7 @@ AddRelationNotNullConstraints(Relation rel, List *constraints,
  * relcache entries for the rel.  Also, this backend will rebuild its
  * own relcache entry at the next CommandCounterIncrement.
  */
-static void
+void
 SetRelationNumChecks(Relation rel, int numchecks)
 {
 	Relation	relrel;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 93d46d85b8..2545320426 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -21118,6 +21118,191 @@ moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPar
 	}
 }
 
+
+/*
+	/* Reproduce not-null constraints. */
+	if (constr->has_not_null)
+		List	   *nnconstraints;
+		nnconstraints = RelationGetNotNullConstraints(RelationGetRelid(modelRel),
+													  false, true);
+		Assert(list_length(nnconstraints) > 0);
+		AddRelationNotNullConstraints(newRel, nnconstraints, NULL);
@@ -21129,13 +21314,14 @@ moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPar
 
@@ -21144,56 +21330,53 @@ createPartitionTable(RangeVar *newPartName, Relation modelRel,

 	/*
 
 	/*
 	 * We intended to create the partition with the same persistence as the
@@ -21216,6 +21399,9 @@ createPartitionTable(RangeVar *newPartName, Relation modelRel,
 				 errmsg("cannot create a permanent relation as partition of temporary relation \"%s\"",
 						RelationGetRelationName(modelRel))));

@@ -21313,7 +21499,7 @@ ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
 		Relation	newPartRel;

@@ -21557,7 +21743,7 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	}

 	/* Copy data from merged partitions to new partition. */
 	moveMergedTablesRows(rel, mergingPartitionsList, newPartRel);
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index 8c278f202b..afa5e8628a 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -118,6 +118,12 @@ extern List *AddRelationNotNullConstraints(Relation rel,
 										   List *constraints,
 										   List *old_notnulls);
 
+						 bool is_validated, bool is_local, int16 inhcount,
 
index 0128f53d93..a47a635e50 100644
@@ -1584,6 +1584,65 @@ ALTER TABLE t SPLIT PARTITION tp_0 INTO

 --
-- 
2.40.1.windows.1

---

From: stephane tachoires <[email-masked]>
Date: Mon, 27 Jan 2025 22:24:16 +0000

Only patches v34-0001 and 2 has been tested.
Patch v34-0003-Refactor-createPartitionTable-to-remove-ProcessU.patch do not apply anymore on src/backend/catalog/heap.c

The new status of this patch is: Waiting on Author

---

From: Dmitry Koval <[email-masked]>
Date: Tue, 28 Jan 2025 02:14:52 +0300

>Patch v34-0003-Refactor-createPartitionTable-to-remove-ProcessU.patch
 >do not apply anymore on src/backend/catalog/heap.c

Thanks, rebased.

-- 
With best regards,
Dmitry Koval

Postgres Professional: http://postgrespro.com
From 14ebf8cb9ea7688e8401637e7349d14bdf6942d6 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:57:22 +0300
Subject: [PATCH v35 1/3] Implement ALTER TABLE ... MERGE PARTITIONS ...
 command

index f9576da435..a2e4e37034 100644

@@ -1118,14 +1121,85 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>

   <para>
@@ -1368,7 +1442,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1786,6 +1861,13 @@ ALTER TABLE measurement
 
+  <para>
 
  <refsect1>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d617c4bc63..922dd53d09 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -701,6 +701,8 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, const char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);

 /* ----------------------------------------------------------------
  *		DefineRelation
@@ -4789,6 +4791,10 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = ShareUpdateExclusiveLock;
 				break;
 
+			case AT_MergePartitions:
+				cmd_lockmode = AccessExclusiveLock;
+				break;
+
 			default:			/* oops */
 				elog(ERROR, "unrecognized alter table type: %d",
 					 (int) cmd->subtype);
@@ -5222,6 +5228,11 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* No command-specific prep needed */
 			pass = AT_PASS_MISC;
 			break;
+		case AT_MergePartitions:
+			ATSimplePermissions(cmd->subtype, rel, ATT_PARTITIONED_TABLE);
+			/* No command-specific prep needed */
+			pass = AT_PASS_MISC;
+			break;
 		default:				/* oops */
 			elog(ERROR, "unrecognized alter table type: %d",
 				 (int) cmd->subtype);
@@ -5616,6 +5627,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 		case AT_DetachPartitionFinalize:
 			address = ATExecDetachPartitionFinalize(rel, ((PartitionCmd *) cmd->def)->name);
 			break;
+		case AT_MergePartitions:
+			cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode,
+									  cur_pass, context);
+			Assert(cmd != NULL);
+			Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
+			ATExecMergePartitions(wqueue, tab, rel, (PartitionCmd *) cmd->def,
+								  context);
+			break;
 		default:				/* oops */
 			elog(ERROR, "unrecognized alter table type: %d",
 				 (int) cmd->subtype);
@@ -6600,6 +6619,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
 			return "DETACH PARTITION";
 		case AT_DetachPartitionFinalize:
 			return "DETACH PARTITION ... FINALIZE";
+		case AT_MergePartitions:
+			return "MERGE PARTITIONS";
 		case AT_AddIdentity:
 			return "ALTER COLUMN ... ADD IDENTITY";
 		case AT_SetIdentity:
@@ -19199,6 +19220,37 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
 	}
 }
 
+/*
@@ -19402,23 +19454,8 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -21050,3 +21087,349 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
index d7f9c00c40..f2787452e6 100644
@@ -756,7 +756,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -2332,6 +2332,7 @@ partition_cmd:
 
@@ -2346,6 +2347,7 @@ partition_cmd:
 					n->subtype = AT_DetachPartition;
 					cmd->name = $3;
 					cmd->bound = NULL;
+					cmd->partlist = NULL;
 					cmd->concurrent = $4;
 					n->def = (Node *) cmd;
 
@@ -2359,6 +2361,21 @@ partition_cmd:
@@ -2375,6 +2392,7 @@ index_partition_cmd:
 
@@ -17871,6 +17889,7 @@ unreserved_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PATH
@@ -18496,6 +18515,7 @@ bare_label_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PATH
index ca028d2a66..5754a97683 100644
@@ -3485,6 +3488,116 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
 }

+/*
+ * checkPartition
@@ -3762,6 +3875,19 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,

 				/*
index 4bdc2941ef..cb719f1ff5 100644
 
 						/*
 
index 81cbf10aa2..5c1393232a 100644
 
index ffe155ee20..36e53ef300 100644
@@ -948,6 +948,17 @@ typedef struct PartitionRangeDatum
 
+/*
@@ -956,6 +967,8 @@ typedef struct PartitionCmd
 
@@ -2463,6 +2476,7 @@ typedef enum AlterTableType
index cf2917ad07..a0b21ced88 100644
@@ -336,6 +336,7 @@ PG_KEYWORD("parameter", PARAMETER, UNRESERVED_KEYWORD, BARE_LABEL)
index 65f161f718..690d259619 100644
 
index 193669f2bc..7de5ddb878 100644
index 1edd9e45eb..71a5d73143 100644
 
index a2644a2e65..7bbcf20ef3 100644
@@ -2682,6 +2682,7 @@ SimpleStats
 SimpleStringList
 SimpleStringListCell
 SingleBoundSortItem
+SinglePartitionSpec
 Size
 SkipPages
 SlabBlock
-- 
2.40.1.windows.1

From 82e4e90f322ca811eee935beef7c426bf78ef921 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:58:09 +0300
Subject: [PATCH v35 2/3] Implement ALTER TABLE ... SPLIT PARTITION ... command

 
index a2e4e37034..98ce143dbb 100644
@@ -1121,6 +1125,56 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>

@@ -1199,7 +1253,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1443,7 +1498,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1861,6 +1916,24 @@ ALTER TABLE measurement
 
+  <para>
index 922dd53d09..3b7cab0d42 100644
@@ -701,6 +701,9 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);

@@ -4791,6 +4794,10 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = ShareUpdateExclusiveLock;
 				break;
 
@@ -5228,6 +5235,11 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
@@ -5627,6 +5639,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
@@ -6619,6 +6639,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -21088,6 +21110,256 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 	return cstorage;
 }
 
+/*
@@ -21189,6 +21461,132 @@ createPartitionTable(RangeVar *newPartName, Relation modelRel,
 	return newRel;
 }
 
+/*
index f2787452e6..d43e3f2791 100644
@@ -258,6 +258,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -641,6 +642,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -771,7 +774,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -2322,6 +2325,23 @@ alter_table_cmds:
 			| alter_table_cmds ',' alter_table_cmd	{ $$ = lappend($1, $3); }
 		;
 
+partitions_list:
@@ -2366,6 +2386,20 @@ partition_cmd:
@@ -17958,6 +17992,7 @@ unreserved_keyword:
@@ -18596,6 +18631,7 @@ bare_label_keyword:
index 5754a97683..9f86c4f056 100644
@@ -3526,6 +3526,40 @@ checkPartition(Relation rel, Oid partRelOid)
 
+/*
@@ -3867,7 +3901,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3875,6 +3909,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3883,7 +3918,11 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
@@ -4316,13 +4355,13 @@ setSchemaName(const char *context_schema, char **stmt_schema_name)
 
 /*
  * transformPartitionCmd
 
@@ -4331,9 +4370,9 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 
@@ -4341,7 +4380,7 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
index cb719f1ff5..9a3a281c84 100644
 
+/*
 
+/*
index 54dad97555..249fa70234 100644
@@ -13618,3 +13618,21 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+/*
index 5c1393232a..9d37fc9317 100644
 
 	/*
 
index 36e53ef300..3a88287b5d 100644
@@ -2476,6 +2476,7 @@ typedef enum AlterTableType
index a0b21ced88..1c03c74f1c 100644
@@ -420,6 +420,7 @@ PG_KEYWORD("smallint", SMALLINT, COL_NAME_KEYWORD, BARE_LABEL)
index 690d259619..7765a9d365 100644
 
index aa7a8a3800..52bd8aa51e 100644

index 7de5ddb878..17d72e412f 100644
index 71a5d73143..9bf7c37202 100644
 
index 7bbcf20ef3..60c9383d0a 100644
@@ -2746,6 +2746,7 @@ SpecialJoinInfo
 SpinDelayStatus
 SplitInterval
 SplitLR
+SplitPartitionContext
 SplitPageLayout
 SplitPoint
 SplitTextOutputData
-- 
2.40.1.windows.1

From 856e6e71ac212ce9bf053ce47d4558a41e2d7953 Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email-masked]>
Date: Thu, 29 Aug 2024 20:27:03 +0300
Subject: [PATCH v35 3/3] Refactor createPartitionTable to remove
 ProcessUtility call

---
 src/backend/catalog/heap.c                    |   8 +-
 src/backend/commands/tablecmds.c              | 291 ++++++++++++++----
 src/include/catalog/heap.h                    |   6 +
 src/test/regress/expected/partition_split.out |  59 ++++
 src/test/regress/sql/partition_split.sql      |  21 ++
 5 files changed, 327 insertions(+), 58 deletions(-)

diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 57ef466acc..1476d0ec04 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -101,9 +101,6 @@ static ObjectAddress AddNewRelationType(const char *typeName,
 										Oid new_row_type,
 										Oid new_array_type);
 static void RelationRemoveInheritance(Oid relid);
-static Oid	StoreRelCheck(Relation rel, const char *ccname, Node *expr,
-						  bool is_enforced, bool is_validated, bool is_local,
-						  int16 inhcount, bool is_no_inherit, bool is_internal);
 static void StoreConstraints(Relation rel, List *cooked_constraints,
 							 bool is_internal);
 static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
@@ -111,7 +108,6 @@ static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *
 										bool is_enforced,
 										bool is_initially_valid,
 										bool is_no_inherit);
-static void SetRelationNumChecks(Relation rel, int numchecks);
 static Node *cookConstraint(ParseState *pstate,
 							Node *raw_constraint,
 							char *relname);
@@ -2065,7 +2061,7 @@ SetAttrMissing(Oid relid, char *attname, char *value)
  *
  * The OID of the new constraint is returned.
  */
-static Oid
+Oid
 StoreRelCheck(Relation rel, const char *ccname, Node *expr,
 			  bool is_enforced, bool is_validated, bool is_local,
 			  int16 inhcount, bool is_no_inherit, bool is_internal)
@@ -3065,7 +3061,7 @@ AddRelationNotNullConstraints(Relation rel, List *constraints,
  * relcache entries for the rel.  Also, this backend will rebuild its
  * own relcache entry at the next CommandCounterIncrement.
  */
-static void
+void
 SetRelationNumChecks(Relation rel, int numchecks)
 {
 	Relation	relrel;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 3b7cab0d42..3dcb8595e5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -21360,6 +21360,192 @@ moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPar
 	}
 }
 
+
+/*
+		bool		ccenforced = constr->check[ccnum].ccenforced;
+		(void) StoreRelCheck(newRel, ccname, ccbin_node, ccenforced, true,
+							 true, 0, ccnoinherit, false);
@@ -21371,13 +21557,14 @@ moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPar
 
@@ -21386,56 +21573,53 @@ createPartitionTable(RangeVar *newPartName, Relation modelRel,

 	/*
 
 	/*
 	 * We intended to create the partition with the same persistence as the
@@ -21458,6 +21642,9 @@ createPartitionTable(RangeVar *newPartName, Relation modelRel,
 				 errmsg("cannot create a permanent relation as partition of temporary relation \"%s\"",
 						RelationGetRelationName(modelRel))));

@@ -21555,7 +21742,7 @@ ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
 		Relation	newPartRel;

@@ -21799,7 +21986,7 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	}

 	/* Copy data from merged partitions to new partition. */
 	moveMergedTablesRows(rel, mergingPartitionsList, newPartRel);
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index cad830dc39..05cd4f4256 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -119,6 +119,12 @@ extern List *AddRelationNotNullConstraints(Relation rel,
 										   List *constraints,
 										   List *old_notnulls);
 
+extern Oid StoreRelCheck(Relation rel, const char *ccname, Node *expr,
+						 bool is_enforced, bool is_validated, bool is_local,
+						 int16 inhcount, bool is_no_inherit, bool is_internal);
+
+extern void SetRelationNumChecks(Relation rel, int numchecks);
+
 extern void RelationClearMissing(Relation rel);
 extern void SetAttrMissing(Oid relid, char *attname, char *value);

 
 --
-- 
2.40.1.windows.1

---

From: Alexander Korotkov <[email-masked]>
Date: Fri, 31 Jan 2025 11:07:17 +0200

Hi, Dmitry!

> The patches are attached to the email.

Thank you for the rebase.
I don't think we need a separate 0003 patch with refactoring.  It's
probably good idea to keep this functionality as a separate patch, but
let's make then it a 0001, which prepares functions used by 0002 and
0003.

------
Regards,
Alexander Korotkov
Supabase

---

From: Alexander Korotkov <[email-masked]>
Date: Fri, 31 Jan 2025 11:19:29 +0200

Hi!

I'd like to share some thoughts on which particular way this patch could go.

> SECURITY_RESTRICTED_OPERATION here.

+1 for use SECURITY_RESTRICTED_OPERATION

> annoying things to turn out to also be insecure.

Yes, I think it's a good idea to duplicate dependent objects of split
partition to new partitions.  But it important to very carefully check
user have relevant permissions for all of them.  We could also provide
a syntax to exclude some of them (and even define new ones?), but I
strongly suspect that would overcomplicate patch for now and we need
to postpone this.

Regarding the merge, I think it would be good to provide a syntax to
let user choose a model partition between partitions to be merged.

> likely and we don't really need it. I don't know.

Hmm... I think the important aspect for this DDL operation is to be
atomic and transactional.  And that seems to be extremely hard to
achieve if we move the data between existing relnodes.  How can we
rollback or recover after error?  So, it least for initial
implementation I would leave data movement as it is.

------
Regards,
Alexander Korotkov
Supabase

---

From: Alexander Korotkov <[email-masked]>
Date: Fri, 31 Jan 2025 11:22:51 +0200

> and in what order they should be implemented.

Please, check my thoughts on how this patch could be further
developed.  Given amount of work to be done, I doubt that'a a subject
for pg18.  But I think you could continue this work, and we could
consider it for early pg19 cycle.

Links.
1. https://www.postgresql.org/message-id/CAPpHfdtSxrcxQERO82cyQ2heN3%2BA7VC63k8SmL%3DEEiph-8rfHg%40mail.gmail.com
2. https://www.postgresql.org/message-id/CAPpHfdvVMdUX0DGSK3oAbt9C5TPup%3DBEq8QkmvDrMbuD4BR9Fw%40mail.gmail.com

------
Regards,
Alexander Korotkov
Supabase

---

From: Dmitry Koval <[email-masked]>
Date: Mon, 3 Feb 2025 18:37:47 +0300

Hi, Alexander!
Thanks for your advices and recommendations!

 >let's make then it a 0001, which prepares functions used by 0002 and

Done. 0003 was created separately to better understand what changes were 
made after the verified changes 0001 and 0002.

 >consider it for early pg19 cycle.

Good. I'll try to collect and summarize the opinions of colleagues on 
these issues [1], and then put them up for discussion in this thread.

Links.
[1] 
https://www.postgresql.org/message-id/CAPpHfdtSxrcxQERO82cyQ2heN3%2BA7VC63k8SmL%3DEEiph-8rfHg%40mail.gmail.com

-- 
With best regards,
Dmitry Koval

Postgres Professional: http://postgrespro.com
From bcef7618e89ef60483a85853f4bba83bd4416dc6 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:57:22 +0300
Subject: [PATCH v36 1/2] Implement ALTER TABLE ... MERGE PARTITIONS ...
 command

 src/backend/commands/tablecmds.c              | 603 ++++++++++-
 20 files changed, 2901 insertions(+), 30 deletions(-)

index 7ff39ae8c6..7f4282504b 100644
@@ -4418,6 +4418,25 @@ ALTER TABLE measurement_y2006m02 ADD UNIQUE (city_id, logdate);

 
 
   <para>
 
+  <para>
 
  <refsect1>
index 18f64db6e3..1bd9cb10b7 100644

 
@@ -19183,6 +19204,37 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
 	}
 }
 
+/*
@@ -19386,23 +19438,8 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -21034,3 +21071,535 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*

 

+/*
+ * checkPartition

 				/*
 
 						/*

 

+/*

 
index 9a3bee93de..b8ac4a92eb 100644

From 32630a6b63cffcc1033aaa600462db26f35b8739 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:58:09 +0300
Subject: [PATCH v36 2/2] Implement ALTER TABLE ... SPLIT PARTITION ... command

 src/backend/commands/tablecmds.c              |  399 ++++
 src/test/regress/expected/partition_split.out | 1649 +++++++++++++++++
 src/test/regress/sql/partition_split.sql      |  983 ++++++++++
 20 files changed, 4184 insertions(+), 17 deletions(-)

index 7f4282504b..a059e85090 100644
@@ -4439,6 +4439,25 @@ ALTER TABLE measurement

 
 
+  <para>
index 1bd9cb10b7..eb30791580 100644

 
@@ -21072,6 +21094,257 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 	return cstorage;
 }
 
+/*
  * getAttributesList: return list of columns (ColumnDef) like model table
  * (modelRel)
@@ -21359,6 +21632,132 @@ createPartitionTable(RangeVar *newPartName, Relation modelRel)
 	return newRel;
 }
 
+/*

 
+partitions_list:
 
+/*

 
 /*
  * transformPartitionCmd

 
+/*
 
+/*
 
 	return buf->data;
 }
+
+/*
 
 	/*

 
 
index 0000000000..a47a635e50
@@ -0,0 +1,1649 @@
 
index 0000000000..e185458e4e
@@ -0,0 +1,983 @@
index b8ac4a92eb..1c9b5df9ad 100644

---

From: Álvaro Herrera <[email-masked]>
Date: Mon, 3 Feb 2025 17:40:18 +0100

> -static void SetRelationNumChecks(Relation rel, int numchecks);

I'm wary of taking some static functions and making them non-static
without further analysis, because we create a kind-of API that's
possible incoherent or at least incomplete and may cause trouble later.
Maybe we should make some effort to construct a real interface here.
For example, perhaps you don't need to expose both StoreRelCheck and
SetRelationNumChecks, but instead would be better served by exposing
StoreConstraints?  (Though I think we need some explicit check that that
function requires that the table is new and has no constraint, otherwise
SetRelationNumChecks stores the wrong thing.  No?)  Or maybe use
AddRelationNewConstraints() instead, which is an already exposed function.

What I'm saying is just that we should be more watchful of what we put
in our .h files.

Note: I didn't actually review the patch.

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/

---

From: vignesh C <[email-masked]>
Date: Mon, 17 Mar 2025 12:25:16 +0530

> these issues [1], and then put them up for discussion in this thread.

I noticed that Alvaro's comments from [1] have not yet been addressed,
I have changed the status of commitfest entry to "Waiting on Author",
please address them and update it to "Needs review".
[1] - https://www.postgresql.org/message-id/[email-masked]

Regards,
Vignesh

---

From: Dmitry Koval <[email-masked]>
Date: Mon, 17 Mar 2025 16:36:56 +0300

> I noticed that Alvaro's comments from [1] have not yet been addressed,

Thanks!
I'm sorry, I missed this comment.

...
 > StoreConstraints? ... Or maybe use AddRelationNewConstraints() ...

I replaced explosion of functions StoreRelCheck + SetRelationNumChecks
to StoreConstraints explosion. Probably function 
AddRelationNewConstraints is not very suitable for this ...

[1] 
https://www.postgresql.org/message-id/[email-masked]

-- 
With best regards,
Dmitry Koval

Postgres Professional: http://postgrespro.com
From aa0d98d5a964564f1ba2d3d2f3d85ae1498c293e Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:57:22 +0300
Subject: [PATCH v37 1/2] Implement ALTER TABLE ... MERGE PARTITIONS ...
 command

 src/backend/catalog/heap.c                    |   4 +-
 src/backend/commands/tablecmds.c              | 617 +++++++++++-
 src/include/catalog/heap.h                    |   3 +
 20 files changed, 2911 insertions(+), 27 deletions(-)

index cdb1a07e9d..0f01dc3cd9 100644
@@ -4442,6 +4442,25 @@ ALTER TABLE measurement_y2006m02 ADD UNIQUE (city_id, logdate);
index 4f15b89a98..3d059813c5 100644

@@ -1144,14 +1147,85 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>

   <para>
@@ -1394,7 +1468,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1818,6 +1893,13 @@ ALTER TABLE measurement
 
+  <para>
 
  <refsect1>
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index bd3554c0bf..39ae742cb8 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -105,8 +105,6 @@ static void RelationRemoveInheritance(Oid relid);
 static Oid	StoreRelCheck(Relation rel, const char *ccname, Node *expr,
 						  bool is_enforced, bool is_validated, bool is_local,
 						  int16 inhcount, bool is_no_inherit, bool is_internal);
-static void StoreConstraints(Relation rel, List *cooked_constraints,
-							 bool is_internal);
 static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
 										bool allow_merge, bool is_local,
 										bool is_enforced,
@@ -2296,7 +2294,7 @@ StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum,
  * expressions can be added later, by direct calls to StoreAttrDefault
  * and StoreRelCheck (see AddRelationNewConstraints()).
  */
-static void
+void
 StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal)
 {
 	int			numchecks = 0;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 129c97fdf2..4b474a190a 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -711,6 +711,8 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, const char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);

 /* ----------------------------------------------------------------
  *		DefineRelation
@@ -4805,6 +4807,10 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = ShareUpdateExclusiveLock;
 				break;
 
+			case AT_MergePartitions:
+				cmd_lockmode = AccessExclusiveLock;
+				break;
+
 			default:			/* oops */
 				elog(ERROR, "unrecognized alter table type: %d",
 					 (int) cmd->subtype);
@@ -5240,6 +5246,11 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* No command-specific prep needed */
 			pass = AT_PASS_MISC;
 			break;
+		case AT_MergePartitions:
+			ATSimplePermissions(cmd->subtype, rel, ATT_PARTITIONED_TABLE);
+			/* No command-specific prep needed */
+			pass = AT_PASS_MISC;
+			break;
 		default:				/* oops */
 			elog(ERROR, "unrecognized alter table type: %d",
 				 (int) cmd->subtype);
@@ -5636,6 +5647,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 		case AT_DetachPartitionFinalize:
 			address = ATExecDetachPartitionFinalize(rel, ((PartitionCmd *) cmd->def)->name);
 			break;
+		case AT_MergePartitions:
+			cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode,
+									  cur_pass, context);
+			Assert(cmd != NULL);
+			Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
+			ATExecMergePartitions(wqueue, tab, rel, (PartitionCmd *) cmd->def,
+								  context);
+			break;
 		default:				/* oops */
 			elog(ERROR, "unrecognized alter table type: %d",
 				 (int) cmd->subtype);
@@ -6620,6 +6639,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
 			return "DETACH PARTITION";
 		case AT_DetachPartitionFinalize:
 			return "DETACH PARTITION ... FINALIZE";
+		case AT_MergePartitions:
+			return "MERGE PARTITIONS";
 		case AT_AddIdentity:
 			return "ALTER COLUMN ... ADD IDENTITY";
 		case AT_SetIdentity:
@@ -19498,6 +19519,37 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
 	}
 }
 
+/*
@@ -19701,23 +19753,8 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -21349,3 +21386,549 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+	List	   *cookedConstraints = NIL;
+			StoreAttrDefault(newRel, num, def, true);
+		CookedConstraint *cooked;
+		cooked = (CookedConstraint *) palloc(sizeof(CookedConstraint));
+		cooked->contype = CONSTR_CHECK;
+		cooked->conoid = InvalidOid;
+		cooked->name = ccname;
+		cooked->attnum = 0;
+		cooked->expr = ccbin_node;
+		cooked->is_enforced = ccenforced;
+		cooked->skip_validation = false;
+		cooked->is_local = true;
+		cooked->inhcount = 0;
+		cooked->is_no_inherit = ccnoinherit;
+		cookedConstraints = lappend(cookedConstraints, cooked);
+	/* Store CHECK constraints. */
+	StoreConstraints(newRel, cookedConstraints, false);
+	/* Don't need the cookedConstraints any more. */
+	list_free_deep(cookedConstraints);
index 271ae26cba..cdd5d590d7 100644

 
 
@@ -17906,6 +17924,7 @@ unreserved_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PATH
@@ -18532,6 +18551,7 @@ bare_label_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PATH
index abbe1bb45a..222612cd9e 100644
@@ -3501,6 +3504,116 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
 }

+/*
+ * checkPartition
@@ -3778,6 +3891,19 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,

 				/*
 
 						/*
 
index 8432be641a..78469c7d6c 100644
@@ -2714,6 +2714,7 @@ match_previous_words(int pattern_id,
@@ -2980,6 +2981,15 @@ match_previous_words(int pattern_id,
 
index dbd339e9df..c130bc0c38 100644
@@ -119,6 +119,9 @@ extern List *AddRelationNotNullConstraints(Relation rel,
 										   List *constraints,
 										   List *old_notnulls);
 
+extern void StoreConstraints(Relation rel, List *cooked_constraints,
+							 bool is_internal);
+
 extern void RelationClearMissing(Relation rel);
 
 extern void StoreAttrMissingVal(Relation rel, AttrNumber attnum,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 23c9e3c5ab..9805b68ba5 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -948,6 +948,17 @@ typedef struct PartitionRangeDatum
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } PartitionRangeDatum;
 
+/*
 
index 40cf090ce6..dd607d9896 100644
 
index 37b6d21e1f..379b961199 100644
 
index 93339ef3c5..347a2b4e15 100644
@@ -2709,6 +2709,7 @@ SimpleStats
 SimpleStringList
 SimpleStringListCell
 SingleBoundSortItem
+SinglePartitionSpec
 Size
 SkipPages
 SlabBlock
-- 
2.40.1.windows.1

From 74d7dfada57d58b64884c176d426629bcf531914 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:58:09 +0300
Subject: [PATCH v37 2/2] Implement ALTER TABLE ... SPLIT PARTITION ... command

index 0f01dc3cd9..1c35baea9a 100644
@@ -4463,6 +4463,25 @@ ALTER TABLE measurement
 
index 3d059813c5..66203c0683 100644
@@ -1147,6 +1151,56 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>

@@ -1225,7 +1279,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1469,7 +1524,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1893,6 +1948,24 @@ ALTER TABLE measurement
 
+  <para>
index 4b474a190a..a01ee0a4c7 100644
@@ -711,6 +711,9 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);

@@ -4807,6 +4810,10 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = ShareUpdateExclusiveLock;
 				break;
 
@@ -5246,6 +5253,11 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
@@ -5647,6 +5659,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
@@ -6639,6 +6659,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -21387,6 +21409,257 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 	return cstorage;
 }
 
+/*
@@ -21688,6 +21961,132 @@ createPartitionTable(RangeVar *newPartName, Relation modelRel)
 	return newRel;
 }
 
+/*
index cdd5d590d7..7299f16cbf 100644

 
+partitions_list:
@@ -17993,6 +18027,7 @@ unreserved_keyword:
@@ -18632,6 +18667,7 @@ bare_label_keyword:
index 222612cd9e..2dea1c6d2f 100644
@@ -3542,6 +3542,40 @@ checkPartition(Relation rel, Oid partRelOid)
 
+/*
@@ -3883,7 +3917,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3891,6 +3925,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3899,7 +3934,11 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
@@ -4332,13 +4371,13 @@ setSchemaName(const char *context_schema, char **stmt_schema_name)
 
 /*
  * transformPartitionCmd
 
@@ -4347,9 +4386,9 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 
@@ -4357,7 +4396,7 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 
+/*
 
+/*
index 9e90acedb9..6f40c02109 100644
@@ -13697,3 +13697,21 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+/*
index 78469c7d6c..0bb693e63d 100644
@@ -2714,7 +2714,7 @@ match_previous_words(int pattern_id,
@@ -2970,10 +2970,10 @@ match_previous_words(int pattern_id,
 
 	/*
@@ -2981,6 +2981,10 @@ match_previous_words(int pattern_id,
 
index 9805b68ba5..cd3e854d47 100644
index dd607d9896..6b4f65d313 100644
 
+extern void check_partitions_for_split(Relation parent,
+									   Oid splitPartOid,
+									   RangeVar *splitPartName,
+									   List *partlist,
+									   ParseState *pstate);
 extern void calculate_partition_bound_for_merge(Relation parent,
 												List *partNames,
 												List *partOids,
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 5f2ea2e4d0..62043d3bf5 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -54,4 +54,6 @@ extern char *get_range_partbound_string(List *bound_datums);

index 379b961199..21c1fa730a 100644
 
index 347a2b4e15..5d25679129 100644
@@ -2774,6 +2774,7 @@ SpecialJoinInfo
 SpinDelayStatus
 SplitInterval
 SplitLR
+SplitPartitionContext
 SplitPageLayout
 SplitPoint
 SplitTextOutputData
-- 
2.40.1.windows.1

---

From: Dmitry Koval <[email-masked]>
Date: Mon, 12 May 2025 11:31:04 +0300

Hi!

We (with colleagues) discussed further improvements to SPLIT/MERGE 
PARTITION(S). As a result of the discussion, the following answers to 
the questions remained:

1. Who should be the owner of new partitions (SPLIT PARTITION command) 
if owner of the partition being split is not the same as the current user?
a) current user (since he is the one who creates new tables);
b) the owner of the partitioned partition (since it is the owner of the 
table and should continue to own it).

2. Who should be the owner of the new partition (MERGE PARTITIONS 
command) if the partitions being merged have different owners?
a) current user (since he is the one who creates new table);
b) merging of partitions should be forbidden if they have different owners.

Please, advise what seems to be the best solution for points 1 and 2.

With best regards,
Dmitry Koval

---

From: Dmitry Koval <[email-masked]>
Date: Tue, 20 May 2025 01:35:45 +0300

Hi!

Changes in patches:

1) Added usage of SECURITY_RESTRICTED_OPERATION for SPLIT/MERGE 
PARTITION(S) commands.

2) For SPLIT PARTITION command: new partitions will have the same owner 
as the parent.

3) For MERGE PARTITIONS command: if merged partitions have different 
owners, an error will be generated.

Patches are attached to the email.

-- 
With best regards,
Dmitry Koval

Postgres Professional: http://postgrespro.com
From 52344bea006e5bd8d4329f89473525e0f1ff59ba Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:57:22 +0300
Subject: [PATCH v38 1/2] Implement ALTER TABLE ... MERGE PARTITIONS ...
 command

 doc/src/sgml/ref/alter_table.sgml             |  91 +-
 src/backend/commands/tablecmds.c              | 654 +++++++++++-
 src/test/regress/expected/partition_merge.out | 985 ++++++++++++++++++
 src/test/regress/sql/partition_merge.sql      | 642 ++++++++++++
 20 files changed, 3022 insertions(+), 27 deletions(-)

index fcd1cb8535..199d57f4c7 100644
@@ -4439,6 +4439,25 @@ ALTER TABLE measurement_y2006m02 ADD UNIQUE (city_id, logdate);
index d63f3a621a..93d28ad058 100644

@@ -1147,14 +1150,88 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+      If merged partitions have different owners, an error will be generated.
 
   <para>
@@ -1397,7 +1474,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1830,6 +1908,13 @@ ALTER TABLE measurement
 
+  <para>
 
  <refsect1>
index fbaed5359a..571b6b0574 100644
index 54ad38247a..dc44dd4053 100644
@@ -740,6 +740,8 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);

 /* ----------------------------------------------------------------
  *		DefineRelation
@@ -4834,6 +4836,10 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = ShareUpdateExclusiveLock;
 				break;
 
+			case AT_MergePartitions:
+				cmd_lockmode = AccessExclusiveLock;
+				break;
+
 			default:			/* oops */
 				elog(ERROR, "unrecognized alter table type: %d",
 					 (int) cmd->subtype);
@@ -5269,6 +5275,11 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* No command-specific prep needed */
 			pass = AT_PASS_MISC;
 			break;
+		case AT_MergePartitions:
+			ATSimplePermissions(cmd->subtype, rel, ATT_PARTITIONED_TABLE);
+			/* No command-specific prep needed */
+			pass = AT_PASS_MISC;
+			break;
 		default:				/* oops */
 			elog(ERROR, "unrecognized alter table type: %d",
 				 (int) cmd->subtype);
@@ -5665,6 +5676,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 		case AT_DetachPartitionFinalize:
 			address = ATExecDetachPartitionFinalize(rel, ((PartitionCmd *) cmd->def)->name);
 			break;
+		case AT_MergePartitions:
+			cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode,
+									  cur_pass, context);
+			Assert(cmd != NULL);
+			Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
+			ATExecMergePartitions(wqueue, tab, rel, (PartitionCmd *) cmd->def,
+								  context);
+			break;
 		default:				/* oops */
 			elog(ERROR, "unrecognized alter table type: %d",
 				 (int) cmd->subtype);
@@ -6705,6 +6724,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
 			return "DETACH PARTITION";
 		case AT_DetachPartitionFinalize:
 			return "DETACH PARTITION ... FINALIZE";
+		case AT_MergePartitions:
+			return "MERGE PARTITIONS";
 		case AT_AddIdentity:
 			return "ALTER COLUMN ... ADD IDENTITY";
 		case AT_SetIdentity:
@@ -20143,6 +20164,37 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
 	}
 }
 
+/*
@@ -20346,23 +20398,8 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -22001,3 +22038,586 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+ * (newPartName) like table (modelRel, partitioned table). ownerId is
+ * determined by the partition on which the operation is performed, so it
+ * is passed separately.
+createPartitionTable(RangeVar *newPartName, Relation modelRel, Oid ownerId)
+										ownerId,
+	Oid			ownerId = InvalidOid;
+	Oid			save_userid;
+	int			save_sec_context;
+	int			save_nestlevel;
+		if (OidIsValid(ownerId))
+			/* Do the partitions being merged have different owners? */
+			if (ownerId != mergingPartition->rd_rel->relowner)
+						 errmsg("partitions being merged have different owners")));
+			ownerId = mergingPartition->rd_rel->relowner;
+	Assert(OidIsValid(ownerId));
+	newPartRel = createPartitionTable(cmd->name, rel, ownerId);
+	 * Switch to the table owner's userid, so that any index functions are run
+	 * as that user.  Also lock down security-restricted operations and
+	 * arrange to make GUC variable changes local to this command.
+	 * Need to do it after determine namespace in createPartitionTable call.
+	GetUserIdAndSecContext(&save_userid, &save_sec_context);
+	SetUserIdAndSecContext(ownerId,
+						   save_sec_context | SECURITY_RESTRICTED_OPERATION);
+	save_nestlevel = NewGUCNestLevel();
+	RestrictSearchPath();
+	/* Roll back any GUC changes executed by index functions */
+	AtEOXact_GUC(false, save_nestlevel);
+	/* Restore userid and security context */
+	SetUserIdAndSecContext(save_userid, save_sec_context);
index 0b5652071d..374ce5432a 100644

 
 
@@ -17909,6 +17927,7 @@ unreserved_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PATH
@@ -18536,6 +18555,7 @@ bare_label_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PATH
index 62015431fd..94c0f431e8 100644
@@ -3488,6 +3491,116 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
 }

+/*
+ * checkPartition
@@ -3765,6 +3878,19 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,

 				/*
 
 						/*
 
index ec65ab79fe..4bf1560c89 100644
@@ -2721,6 +2721,7 @@ match_previous_words(int pattern_id,
@@ -2987,6 +2988,15 @@ match_previous_words(int pattern_id,

 
index 4610fc6129..5ec57e3edd 100644
 
+/*
 
@@ -2468,6 +2481,7 @@ typedef enum AlterTableType
index a4af3f717a..90e8cddf8b 100644
@@ -337,6 +337,7 @@ PG_KEYWORD("parameter", PARAMETER, UNRESERVED_KEYWORD, BARE_LABEL)
 
index e3c669a29c..0dca684955 100644
@@ -107,6 +107,7 @@ test: partition-key-update-1
index 0000000000..42d6e8c781
@@ -0,0 +1,985 @@
+-- Test: we can't merge partitions with different owners
+CREATE ROLE regress_partitions_merge_alice;
+CREATE ROLE regress_partitions_merge_bob;
+GRANT ALL ON SCHEMA partitions_merge_schema TO regress_partitions_merge_alice;
+GRANT ALL ON SCHEMA partitions_merge_schema TO regress_partitions_merge_bob;
+SET SESSION AUTHORIZATION regress_partitions_merge_alice;
+CREATE TABLE tp_0_1(i int);
+SET SESSION AUTHORIZATION regress_partitions_merge_bob;
+CREATE TABLE tp_1_2(i int);
+ALTER TABLE t ATTACH PARTITION tp_0_1 FOR VALUES FROM (0) TO (1);
+ALTER TABLE t ATTACH PARTITION tp_1_2 FOR VALUES FROM (1) TO (2);
+-- Owner is 'regress_partitions_merge_alice':
+\dt tp_0_1
+                              List of tables
+         Schema          |  Name  | Type  |             Owner              
+-------------------------+--------+-------+--------------------------------
+ partitions_merge_schema | tp_0_1 | table | regress_partitions_merge_alice
+-- Owner is 'regress_partitions_merge_bob':
+\dt tp_1_2
+                             List of tables
+         Schema          |  Name  | Type  |            Owner             
+-------------------------+--------+-------+------------------------------
+ partitions_merge_schema | tp_1_2 | table | regress_partitions_merge_bob
+-- ERROR:  partitions being merged have different owners
+ERROR:  partitions being merged have different owners
+REVOKE ALL ON SCHEMA partitions_merge_schema FROM regress_partitions_merge_alice;
+REVOKE ALL ON SCHEMA partitions_merge_schema FROM regress_partitions_merge_bob;
+DROP ROLE regress_partitions_merge_alice;
+DROP ROLE regress_partitions_merge_bob;
index a424be2a6b..6464a238ac 100644
@@ -123,7 +123,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
-test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate numa
+test: partition_merge partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate numa
 
index 0000000000..d862a62741
@@ -0,0 +1,642 @@
+-- Test: we can't merge partitions with different owners
+CREATE ROLE regress_partitions_merge_alice;
+CREATE ROLE regress_partitions_merge_bob;
+GRANT ALL ON SCHEMA partitions_merge_schema TO regress_partitions_merge_alice;
+GRANT ALL ON SCHEMA partitions_merge_schema TO regress_partitions_merge_bob;
+SET SESSION AUTHORIZATION regress_partitions_merge_alice;
+CREATE TABLE tp_0_1(i int);
+SET SESSION AUTHORIZATION regress_partitions_merge_bob;
+CREATE TABLE tp_1_2(i int);
+ALTER TABLE t ATTACH PARTITION tp_0_1 FOR VALUES FROM (0) TO (1);
+ALTER TABLE t ATTACH PARTITION tp_1_2 FOR VALUES FROM (1) TO (2);
+-- Owner is 'regress_partitions_merge_alice':
+\dt tp_0_1
+-- Owner is 'regress_partitions_merge_bob':
+\dt tp_1_2
+-- ERROR:  partitions being merged have different owners
+REVOKE ALL ON SCHEMA partitions_merge_schema FROM regress_partitions_merge_alice;
+REVOKE ALL ON SCHEMA partitions_merge_schema FROM regress_partitions_merge_bob;
+DROP ROLE regress_partitions_merge_alice;
+DROP ROLE regress_partitions_merge_bob;
index 9ea573fae2..20428af1b6 100644
@@ -2764,6 +2764,7 @@ SimpleStats
 SimpleStringList
 SimpleStringListCell
 SingleBoundSortItem
+SinglePartitionSpec
 Size
 SkipPages
 SkipSupport
-- 
2.40.1.windows.1

From b98bb33957a303756621e4a38b509b7121730a2e Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:58:09 +0300
Subject: [PATCH v38 2/2] Implement ALTER TABLE ... SPLIT PARTITION ... command

 doc/src/sgml/ref/alter_table.sgml             |   80 +-
 src/backend/commands/tablecmds.c              |  422 ++++
 src/test/regress/expected/partition_split.out | 1715 +++++++++++++++++
 src/test/regress/sql/partition_split.sql      | 1043 ++++++++++
 20 files changed, 4336 insertions(+), 17 deletions(-)

index 199d57f4c7..27943b5fff 100644
@@ -4460,6 +4460,25 @@ ALTER TABLE measurement
 
index 93d28ad058..cca1090e27 100644
@@ -1150,6 +1154,59 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+      New partitions will have the same owner as the parent.
 
@@ -1231,7 +1288,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1475,7 +1533,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1908,6 +1966,24 @@ ALTER TABLE measurement
 
+  <para>
index dc44dd4053..0044a6485e 100644
@@ -740,6 +740,9 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);

@@ -4836,6 +4839,10 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = ShareUpdateExclusiveLock;
 				break;
 
@@ -5275,6 +5282,11 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
@@ -5676,6 +5688,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
@@ -6724,6 +6744,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -22039,6 +22061,257 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 	return cstorage;
 }
 
+/*
@@ -22342,6 +22615,155 @@ createPartitionTable(RangeVar *newPartName, Relation modelRel, Oid ownerId)
 	return newRel;
 }
 
+/*
+	Oid			save_userid;
+	int			save_sec_context;
+	int			save_nestlevel;
+		newPartRel = createPartitionTable(sps->name, rel,
+										  splitRel->rd_rel->relowner);
+	 * Switch to the table owner's userid, so that any index functions are run
+	 * as that user.  Also lock down security-restricted operations and
+	 * arrange to make GUC variable changes local to this command.
+	 * Need to do it after determine namespace in createPartitionTable call.
+	GetUserIdAndSecContext(&save_userid, &save_sec_context);
+	SetUserIdAndSecContext(splitRel->rd_rel->relowner,
+						   save_sec_context | SECURITY_RESTRICTED_OPERATION);
+	save_nestlevel = NewGUCNestLevel();
+	RestrictSearchPath();
+	/* Roll back any GUC changes executed by index functions */
+	AtEOXact_GUC(false, save_nestlevel);
+	/* Restore userid and security context */
+	SetUserIdAndSecContext(save_userid, save_sec_context);
index 374ce5432a..1f14aa1d70 100644

 
+partitions_list:
@@ -17996,6 +18030,7 @@ unreserved_keyword:
@@ -18636,6 +18671,7 @@ bare_label_keyword:
index 94c0f431e8..85b159b3c6 100644
@@ -3529,6 +3529,40 @@ checkPartition(Relation rel, Oid partRelOid)
 
+/*
@@ -3870,7 +3904,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3878,6 +3912,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3886,7 +3921,11 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
@@ -4321,13 +4360,13 @@ setSchemaName(const char *context_schema, char **stmt_schema_name)
 
 /*
  * transformPartitionCmd
 
@@ -4336,9 +4375,9 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 
@@ -4346,7 +4385,7 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 
+/*
 
+/*
index 3d6e6bdbfd..3e1689b118 100644
@@ -13707,3 +13707,21 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+/*
index 4bf1560c89..20ac6995d3 100644
@@ -2721,7 +2721,7 @@ match_previous_words(int pattern_id,
@@ -2977,10 +2977,10 @@ match_previous_words(int pattern_id,
 
 	/*
@@ -2988,6 +2988,10 @@ match_previous_words(int pattern_id,
 
index 5ec57e3edd..ddd0f71a22 100644
@@ -2481,6 +2481,7 @@ typedef enum AlterTableType
index 90e8cddf8b..66c8876657 100644
@@ -421,6 +421,7 @@ PG_KEYWORD("smallint", SMALLINT, COL_NAME_KEYWORD, BARE_LABEL)

 
index 0dca684955..404a7fd832 100644
@@ -108,6 +108,7 @@ test: partition-key-update-2
index 0000000000..2c1db143ef
@@ -0,0 +1,1715 @@
+-- Test: owner of new partitions should be the same as owner of split partition
+CREATE TABLE tp_0_2(i int);
+ALTER TABLE t ATTACH PARTITION tp_0_2 FOR VALUES FROM (0) TO (2);
+-- Owner is 'regress_partition_split_alice':
+\dt tp_0_2
+                             List of tables
+         Schema         |  Name  | Type  |             Owner             
+------------------------+--------+-------+-------------------------------
+ partition_split_schema | tp_0_2 | table | regress_partition_split_alice
+-- Owner should be 'regress_partition_split_alice':
+\dt tp_0_1
+                             List of tables
+         Schema         |  Name  | Type  |             Owner             
+------------------------+--------+-------+-------------------------------
+ partition_split_schema | tp_0_1 | table | regress_partition_split_alice
+\dt tp_1_2
+                             List of tables
+         Schema         |  Name  | Type  |             Owner             
+------------------------+--------+-------+-------------------------------
+ partition_split_schema | tp_1_2 | table | regress_partition_split_alice
+-- Test: index of new partitions should be created with same owner as split
+-- partition
+CREATE TABLE tp_10_20 PARTITION OF t FOR VALUES FROM (10) TO (20);
+INSERT INTO t VALUES (11), (16);
+CREATE OR REPLACE FUNCTION run_me(integer) RETURNS integer AS $$
+  RAISE NOTICE 'you are running me as %', CURRENT_USER;
+  RETURN $1;
+END
+$$ LANGUAGE PLPGSQL IMMUTABLE;
+-- Owner is 'regress_partition_split_alice':
+CREATE INDEX ON t (run_me(i));
+NOTICE:  you are running me as regress_partition_split_alice
+NOTICE:  you are running me as regress_partition_split_alice
+-- Owner should be 'regress_partition_split_alice':
+ALTER TABLE t SPLIT PARTITION tp_10_20 INTO
+  (PARTITION tp_10_15 FOR VALUES FROM (10) TO (15),
+   PARTITION tp_15_20 FOR VALUES FROM (15) TO (20));
+NOTICE:  you are running me as regress_partition_split_alice
+NOTICE:  you are running me as regress_partition_split_alice
+DROP FUNCTION run_me(integer);
index 6464a238ac..a98aef7ca1 100644
@@ -123,7 +123,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
-test: partition_merge partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate numa
+test: partition_merge partition_split partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate numa
 
index 0000000000..385e02e123
@@ -0,0 +1,1043 @@
+-- Test: owner of new partitions should be the same as owner of split partition
+CREATE TABLE tp_0_2(i int);
+ALTER TABLE t ATTACH PARTITION tp_0_2 FOR VALUES FROM (0) TO (2);
+-- Owner is 'regress_partition_split_alice':
+\dt tp_0_2
+-- Owner should be 'regress_partition_split_alice':
+\dt tp_0_1
+\dt tp_1_2
+-- Test: index of new partitions should be created with same owner as split
+-- partition
+CREATE TABLE tp_10_20 PARTITION OF t FOR VALUES FROM (10) TO (20);
+INSERT INTO t VALUES (11), (16);
+CREATE OR REPLACE FUNCTION run_me(integer) RETURNS integer AS $$
+  RAISE NOTICE 'you are running me as %', CURRENT_USER;
+  RETURN $1;
+END
+$$ LANGUAGE PLPGSQL IMMUTABLE;
+-- Owner is 'regress_partition_split_alice':
+CREATE INDEX ON t (run_me(i));
+-- Owner should be 'regress_partition_split_alice':
+ALTER TABLE t SPLIT PARTITION tp_10_20 INTO
+  (PARTITION tp_10_15 FOR VALUES FROM (10) TO (15),
+   PARTITION tp_15_20 FOR VALUES FROM (15) TO (20));
+DROP FUNCTION run_me(integer);
index 20428af1b6..43f27ddee6 100644
@@ -2831,6 +2831,7 @@ SpecialJoinInfo
 SpinDelayStatus
 SplitInterval
 SplitLR
+SplitPartitionContext
 SplitPageLayout
 SplitPoint
 SplitTextOutputData
-- 
2.40.1.windows.1

---

From: jian he <[email-masked]>
Date: Wed, 21 May 2025 08:35:35 +0800

> table and should continue to own it).

per https://www.postgresql.org/docs/current/sql-altertable.html
"You must own the table to use ALTER TABLE."

That means the current user must own the to be SPLITed partition.
the new partitions owner should be current user (the one who did ALTER TABLE)

> b) merging of partitions should be forbidden if they have different owners.
let say:

alter table whatever_range merge partitions
    ( whatever_range_c, whatever_range_de )
    into whatever_range_cde;

In this case, the current user must own
whatever_range_c and whatever_range_de to perform this operation.
and the current user will be the owner of whatever_range_cde.

---

From: Dmitry Koval <[email-masked]>
Date: Wed, 21 May 2025 15:30:01 +0300

Hi!

 >That means the current user must own the to be SPLITed partition.

Last statement may be incorrect (if the logic is different).
Current user can attach another user's partition. If current user can 
change (attach) another user's partition, why can't he split the 
partition? After attach, partition is part of current user's table, and 
current user can change his own table. (Moreover, the owner of the new 
partitions is the same as the owner of the split partition.)

-- 
With best regards,
Dmitry Koval

---

From: jian he <[email-masked]>
Date: Wed, 21 May 2025 22:54:48 +0800

> partitions is the same as the owner of the split partition.)

I think if he can attach another partition, then he can split the partition.
currently ALTER TABLE ATTACH PARTITION requires the current user
both own the partitioned table and the to be attached partition.

for example:

begin;
create role alice;
create role bob;
GRANT all privileges on schema public to alice;
GRANT all privileges on schema public to bob;
set role bob;
create table at_partitioned (a int, b text) partition by range (a);
set role alice;
create table at_part_2 (b text, a int);
set role bob;
alter table at_partitioned attach partition at_part_2 for values from
(1000) to (2000); --should error
rollback;

----------------
begin;
create role alice;
create role bob;
GRANT all privileges on schema public to alice;
GRANT all privileges on schema public to bob;
set role bob;
create table at_partitioned (a int, b text) partition by range (a);
set role alice;
create table at_part_2 (b text, a int);
alter table at_partitioned attach partition at_part_2 for values from
(1000) to (2000); --should error
rollback;

P.S. maybe using sql examples can illustrate the idea more intuitively,
sometimes words may cause confusion.

---

From: Dmitry Koval <[email-masked]>
Date: Wed, 21 May 2025 20:01:06 +0300

> for example:

If in both examples you replace

create role bob;

with

create role bob SUPERUSER;

and in the second example add "set role bob;" before "alter table ..." 
query, then no error will be occur.

-- 
With best regards,
Dmitry Koval

---

From: jian he <[email-masked]>
Date: Thu, 22 May 2025 10:59:10 +0800

> query, then no error will be occur.

That is fine.
superuser can bypass all permission checks.
superuser can attach any table to the partitioned table as he wants.

for example:

begin;
create role alice SUPERUSER;
(1000) to (2000);
rollback;

Am I missing something?

---

From: Dmitry Koval <[email-masked]>
Date: Thu, 22 May 2025 11:57:43 +0300

> superuser can attach any table to the partitioned table as he wants.

That's right.
Using SUPERUSER may be a rare situation, but it needs to be processed.

-- 
With best regards,
Dmitry Koval

---

From: Alexander Korotkov <[email-masked]>
Date: Tue, 3 Jun 2025 12:58:24 +0300

> Using SUPERUSER may be a rare situation, but it needs to be processed.

I suggest that owner of new partitions produced by ALTER TABLE ...
SPLIT should be the same as owner of original partition.  Even if this
operation is done by superuser.  Superuser may explicitly set he owner
if needed.  I think we need to explicitly document this.

------
Regards,
Alexander Korotkov
Supabase

---

From: Alexander Korotkov <[email-masked]>
Date: Tue, 3 Jun 2025 13:12:28 +0300

Hi, Dmitry!

> owners, an error will be generated.

Some notes to the patch.
1) I think we need explicitly document who is the owner of the new
partition(s).  The documentation for MERGE command says all the merged
partitions should have the same owner.  Good, but also we need to
state that the new partition should have the same owner.  Same to the
SPLIT command.
2) I think we also need to describe what happens not just with
ownership, but with ACLs.  We currently don't do anything to them, so
basically we discard existing ACLs.  It's probably OK, and we could
introduce options for ACL handling later.  But we need to state in the
docs that it's user responsibility to setup ACL on new partition(s).

------
Regards,
Alexander Korotkov
Supabase

---

From: Dmitry Koval <[email-masked]>
Date: Tue, 3 Jun 2025 14:06:09 +0300

> Some notes to the patch. ...

Thanks for the notes!
I'll try to make edits in the next few days.

-- 
With best regards,
Dmitry Koval

---

From: Dmitry Koval <[email-masked]>
Date: Tue, 3 Jun 2025 23:53:13 +0300

Added some changes to documentation.
Patches are attached to the email.

-- 
With best regards,
Dmitry Koval

Postgres Professional: http://postgrespro.com
From dfccd47d858ad71b4d0ce57ade3e629583efd7fa Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:57:22 +0300
Subject: [PATCH v39 1/2] Implement ALTER TABLE ... MERGE PARTITIONS ...
 command

 doc/src/sgml/ref/alter_table.sgml             |  96 +-
 20 files changed, 3027 insertions(+), 27 deletions(-)

index d63f3a621a..72bb53b084 100644

@@ -1147,14 +1150,93 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+      The owner of the merged partitions will be the owner of the new partition.
+      It is the user's responsibility to setup <acronym>ACL</acronym> on the
+      new partition.
 
   <para>
@@ -1397,7 +1479,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1830,6 +1913,13 @@ ALTER TABLE measurement
 
+  <para>
 
  <refsect1>
index acf11e83c0..882fd89609 100644

 
 
+/*

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -22009,3 +22046,586 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*

 

+/*
+ * checkPartition

 				/*
 
 						/*

 
 
 extern void StoreAttrMissingVal(Relation rel, AttrNumber attnum,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index dd00ab420b..a0546fde2d 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -953,6 +953,17 @@ typedef struct PartitionRangeDatum
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } PartitionRangeDatum;
 
+/*
@@ -961,6 +972,8 @@ typedef struct PartitionCmd
 
@@ -2473,6 +2486,7 @@ typedef enum AlterTableType

index a8346cda63..2038603c08 100644
@@ -2760,6 +2760,7 @@ SimpleStats
 SimpleStringList
 SimpleStringListCell
 SingleBoundSortItem
+SinglePartitionSpec
 Size
 SkipPages
 SkipSupport
-- 
2.40.1.windows.1

From be49803869d1a65da4c8150eb41810628e619719 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:58:09 +0300
Subject: [PATCH v39 2/2] Implement ALTER TABLE ... SPLIT PARTITION ... command

 doc/src/sgml/ref/alter_table.sgml             |   84 +-
 20 files changed, 4340 insertions(+), 17 deletions(-)

index 72bb53b084..01e2937092 100644
@@ -1150,6 +1154,63 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+      New partitions will have the same owner as the parent partition.
+      It is the user's responsibility to setup <acronym>ACL</acronym> on new
+      partitions.
 
@@ -1236,7 +1297,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1480,7 +1542,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1913,6 +1975,24 @@ ALTER TABLE measurement
 
+  <para>
index 882fd89609..f9dad4ee50 100644

 
@@ -22047,6 +22069,257 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 	return cstorage;
 }
 
+/*
@@ -22350,6 +22623,155 @@ createPartitionTable(RangeVar *newPartName, Relation modelRel, Oid ownerId)
 	return newRel;
 }
 
+/*

 
+partitions_list:
 
+/*

 
 /*
  * transformPartitionCmd

 
+/*
 
+/*
 
 	return buf->data;
 }
+
+/*
 
 	/*
 
index a0546fde2d..cc9cb8ac42 100644
@@ -2486,6 +2486,7 @@ typedef enum AlterTableType

 
 
index 2038603c08..56ac447288 100644
@@ -2827,6 +2827,7 @@ SpecialJoinInfo
 SpinDelayStatus
 SplitInterval
 SplitLR
+SplitPartitionContext
 SplitPageLayout
 SplitPoint
 SplitTextOutputData
-- 
2.40.1.windows.1

---

From: jian he <[email-masked]>
Date: Wed, 4 Jun 2025 18:45:32 +0800

> Patches are attached to the email.

hi.
I haven't touched v39-0002 yet.
The following are reviews of v39-0001.

+CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree
(sales_date);
+INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31');
+INSERT INTO sales_range VALUES (2,  'Smirnoff', 500,  '2022-02-10');
+INSERT INTO sales_range VALUES (3,  'Ford',     2000, '2022-04-30');
you can put it into one INSERT. like
INSERT INTO sales_range VALUES (1,  'May',      1000, '2022-01-31'),
(1,  'May',      1000, '2022-01-31');
which can make the regress test faster.
(apply the logic to other places in src/test/regress/sql/partition_merge.sql)

This error case doesn't seem to have a related test, and adding one
would be great.

per
https://www.postgresql.org/docs/current/error-message-reporting.html
"The extra parentheses were required before PostgreSQL version 12, but
are now optional."
so now you can remove the extra parentheses.
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("partition of hash-partitioned table cannot be merged"));

we can make the first error message like the second one.
errmsg("\"%s\" is not a partition of \"%s\"....)

+ case AT_MergePartitions:
+ {
+ PartitionCmd *partcmd = (PartitionCmd *) cmd->def;
+
+ if (list_length(partcmd->partlist) < 2)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("list of new partitions should contain at least two items")));
This also seems to have no tests.
adding a dummy one should be ok.

We added List *partlist into PartitionCmd
typedef struct PartitionCmd
{
    NodeTag        type;
    RangeVar   *name;            /* name of partition to attach/detach */
    PartitionBoundSpec *bound;    /* FOR VALUES, if attaching */
    List       *partlist;        /* list of partitions, for MERGE/SPLIT
                                 * PARTITION command */
    bool        concurrent;
} PartitionCmd;
then in src/backend/parser/gram.y
we should use
cmd->partlist = NIL;
instead of
cmd->partlist = NULL;
We also need comments explaining PartitionCmd.name
meaning for ALTER TABLE MERGE PARTITIONS INTO?

transformPartitionCmdForMerge
+ partOid = RangeVarGetRelid(name, NoLock, false);
here "NoLock" seems not right?
For example, after "RangeVarGetRelid(name, NoLock, false);"
before checkPartition, I drop the table test.t in another session.
i got the following error:
ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022,
sales_mar2022, test.t) INTO sales_feb_mar_apr2022;
ERROR:  could not open relation with OID 18164

---

From: Dmitry Koval <[email-masked]>
Date: Wed, 4 Jun 2025 22:44:26 +0300

Hi!
Thank you very much for review.

1.
 >(apply the logic to other places in 
src/test/regress/sql/partition_merge.sql)

Test changed.

2.
 >This error case doesn't seem to have a related test, and adding one

Added test for hash partitioned table.

3.
 >so now you can remove the extra parentheses.

Extra parentheses removed.

4.
 >errmsg("\"%s\" is not a partition of \"%s\"....)

Error message
errmsg("relation \"%s\" is not a partition of relation \"%s\""
occurs in two more places in the code.
I think it's better to keep this error message (for consistency).

5.
 >adding a dummy one should be ok.

Test added.

6.
 >meaning for ALTER TABLE MERGE PARTITIONS INTO?

Fixed.

7.

 >here "NoLock" seems not right?

AccessExclusiveLock on partitioned table protects only the DEFAULT 
partition. Fixed.

P.S. Similar changes were made for the second commit with SPLIT PARTITION.

-- 
With best regards,
Dmitry Koval

From 84dc4d6bdb850b6f13590a80d573b000b8d91e29 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:57:22 +0300
Subject: [PATCH v40 1/2] Implement ALTER TABLE ... MERGE PARTITIONS ...
 command

 src/backend/commands/tablecmds.c              |  654 ++++++++++-
 src/backend/parser/parse_utilcmd.c            |  129 +++
 src/include/nodes/parsenodes.h                |   16 +-
 src/test/regress/expected/partition_merge.out | 1003 +++++++++++++++++
 src/test/regress/sql/partition_merge.sql      |  662 +++++++++++
 20 files changed, 3069 insertions(+), 28 deletions(-)

 

   <para>
 
+  <para>
 
  <refsect1>
index acf11e83c0..b4ec6691d8 100644

 
 
+/*

 	/*
 
 	return cstorage;
 }
+
+/*
+								 RelationGetRelationName(modelRel)));
+							   RelationGetRelationName(modelRel)));
+				errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				errmsg("cannot create as partition of temporary relation of another session"));
+				errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					   RelationGetRelationName(modelRel)));
+				errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					   RelationGetRelationName(modelRel)));
+						errmsg("partitions being merged have different owners"));
+					errcode(ERRCODE_DUPLICATE_TABLE),
+					errmsg("relation \"%s\" already exists", cmd->name->relname));
index 0b5652071d..47823ef7a5 100644
 
+					cmd->partlist = NIL;
 					cmd->concurrent = false;
 					n->def = (Node *) cmd;
 
+					cmd->partlist = NIL;
 					cmd->concurrent = $4;
 					n->def = (Node *) cmd;
 
+					cmd->partlist = NIL;
+					cmd->partlist = NIL;
 					cmd->concurrent = false;
 					n->def = (Node *) cmd;
 
index 62015431fd..0d3ab35a57 100644
@@ -3488,6 +3491,119 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
 }

+/*
+ * checkPartition
+				errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					   RelationGetRelationName(partRel)));
+				errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					   RelationGetRelationName(partRel)));
+				errcode(ERRCODE_UNDEFINED_TABLE),
+					   RelationGetRelationName(rel)));
+						errcode(ERRCODE_DUPLICATE_TABLE),
+						errmsg("partition with name \"%s\" is already used", name->relname),
+		 * Search DEFAULT partition in the list. Lock partitions before
+		 * calculating the boundary for resulting partition.
+		partOid = RangeVarGetRelid(name, AccessShareLock, false);
@@ -3765,6 +3881,19 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
+			case AT_MergePartitions:
+				{
+					PartitionCmd *partcmd = (PartitionCmd *) cmd->def;
+
+					if (list_length(partcmd->partlist) < 2)
+						ereport(ERROR,
+								errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+								errmsg("list of new partitions should contain at least two items"));
+					transformPartitionCmdForMerge(&cxt, partcmd);
+					newcmds = lappend(newcmds, cmd);
+					break;
+				}
+
 			default:
 
 				/*
 
 						/*

 
 
 extern void StoreAttrMissingVal(Relation rel, AttrNumber attnum,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index dd00ab420b..230a42b966 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -953,14 +953,27 @@ typedef struct PartitionRangeDatum
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } PartitionRangeDatum;
 
+/*
-	RangeVar   *name;			/* name of partition to attach/detach */
+	RangeVar   *name;			/* name of partition to attach/detach/merge */

index 0000000000..a24874798d
@@ -0,0 +1,1003 @@
+INSERT INTO sales_range VALUES
+  (1,  'May',      1000, '2022-01-31'),
+  (2,  'Smirnoff', 500,  '2022-02-10'),
+  (3,  'Ford',     2000, '2022-04-30'),
+  (4,  'Ivanov',   750,  '2022-04-13'),
+  (5,  'Deev',     250,  '2022-04-07'),
+  (6,  'Poirot',   150,  '2022-02-11'),
+  (7,  'Li',       175,  '2022-03-08'),
+  (8,  'Ericsson', 185,  '2022-02-23'),
+  (9,  'Muller',   250,  '2022-03-11'),
+  (10, 'Halder',   350,  '2022-01-28'),
+  (11, 'Trump',    380,  '2022-04-06'),
+  (12, 'Plato',    350,  '2022-03-19'),
+  (13, 'Gandi',    377,  '2022-01-09'),
+  (14, 'Smith',    510,  '2022-05-04');
+INSERT INTO sales_range VALUES
+  (1,  'May',      1000, '2022-01-31'),
+  (2,  'Smirnoff', 500,  '2022-02-10'),
+  (3,  'Ford',     2000, '2022-04-30'),
+  (4,  'Ivanov',   750,  '2022-04-13'),
+  (5,  'Deev',     250,  '2022-04-07'),
+  (6,  'Poirot',   150,  '2022-02-11'),
+  (7,  'Li',       175,  '2022-03-08'),
+  (8,  'Ericsson', 185,  '2022-02-23'),
+  (9,  'Muller',   250,  '2022-03-11'),
+  (10, 'Halder',   350,  '2022-01-28'),
+  (11, 'Trump',    380,  '2022-04-06'),
+  (12, 'Plato',    350,  '2022-03-19'),
+  (13, 'Gandi',    377,  '2022-01-09'),
+  (14, 'Smith',    510,  '2022-05-04');
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES
+  ('Manager1', 2021, 12, 7),
+  ('Manager2', 2021, 12, 8),
+  ('Manager3', 2022, 1, 1),
+  ('Manager1', 2022, 2, 4),
+  ('Manager2', 2022, 1, 2),
+  ('Manager3', 2022, 2, 1),
+  ('Manager1', 2022, 3, 3),
+  ('Manager2', 2022, 3, 4),
+  ('Manager3', 2022, 5, 1);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES
+  ('Manager1', 2022, 1, 10),
+  ('Manager2', 2022, 2, 10);
+INSERT INTO salespeople VALUES
+  (1, 'Poirot'),
+  (10, 'May'),
+  (30, 'Ford');
+INSERT INTO sales_range VALUES
+  (1,  'May',      1000, '2022-01-31'),
+  (2,  'Smirnoff', 500,  '2022-02-10'),
+  (3,  'Ford',     2000, '2022-04-30'),
+  (4,  'Ivanov',   750,  '2022-04-13'),
+  (5,  'Deev',     250,  '2022-04-07'),
+  (6,  'Poirot',   150,  '2022-02-11'),
+  (7,  'Li',       175,  '2022-03-08'),
+  (8,  'Ericsson', 185,  '2022-02-23'),
+  (9,  'Muller',   250,  '2022-03-11'),
+  (10, 'Halder',   350,  '2022-01-28'),
+  (11, 'Trump',    380,  '2022-04-06'),
+  (12, 'Plato',    350,  '2022-03-19'),
+  (13, 'Gandi',    377,  '2022-01-09'),
+  (14, 'Smith',    510,  '2022-05-04');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES
+  ('Trump',    'Bejing',         1000, '2022-03-01'),
+  ('Smirnoff', 'New York',        500, '2022-03-03'),
+  ('Ford',     'St. Petersburg', 2000, '2022-03-05'),
+  ('Ivanov',   'Warsaw',          750, '2022-03-04'),
+  ('Deev',     'Lisbon',          250, '2022-03-07'),
+  ('Poirot',   'Berlin',         1000, '2022-03-01'),
+  ('May',      'Helsinki',       1200, '2022-03-06'),
+  ('Li',       'Vladivostok',    1150, '2022-03-09'),
+  ('May',      'Helsinki',       1200, '2022-03-11'),
+  ('Halder',   'Oslo',            800, '2022-03-02'),
+  ('Muller',   'Madrid',          650, '2022-03-05'),
+  ('Smith',    'Kyiv',            350, '2022-03-10'),
+  ('Gandi',    'Warsaw',          150, '2022-03-08'),
+  ('Plato',    'Lisbon',          950, '2022-03-05');
+-- Test for hash partitioned table
+CREATE TABLE t (i int) PARTITION BY HASH(i);
+CREATE TABLE tp1 PARTITION OF t FOR VALUES WITH (MODULUS 2, REMAINDER 0);
+CREATE TABLE tp2 PARTITION OF t FOR VALUES WITH (MODULUS 2, REMAINDER 1);
+-- ERROR:  partition of hash-partitioned table cannot be merged
+ALTER TABLE t MERGE PARTITIONS (tp1, tp2) INTO tp3;
+ERROR:  partition of hash-partitioned table cannot be merged
+-- ERROR:  list of new partitions should contain at least two items
+ALTER TABLE t MERGE PARTITIONS (tp1) INTO tp3;
 
index 0000000000..c53ac5a199
@@ -0,0 +1,662 @@
+INSERT INTO sales_range VALUES
+  (1,  'May',      1000, '2022-01-31'),
+  (2,  'Smirnoff', 500,  '2022-02-10'),
+  (3,  'Ford',     2000, '2022-04-30'),
+  (4,  'Ivanov',   750,  '2022-04-13'),
+  (5,  'Deev',     250,  '2022-04-07'),
+  (6,  'Poirot',   150,  '2022-02-11'),
+  (7,  'Li',       175,  '2022-03-08'),
+  (8,  'Ericsson', 185,  '2022-02-23'),
+  (9,  'Muller',   250,  '2022-03-11'),
+  (10, 'Halder',   350,  '2022-01-28'),
+  (11, 'Trump',    380,  '2022-04-06'),
+  (12, 'Plato',    350,  '2022-03-19'),
+  (13, 'Gandi',    377,  '2022-01-09'),
+  (14, 'Smith',    510,  '2022-05-04');
+INSERT INTO sales_range VALUES
+  (1,  'May',      1000, '2022-01-31'),
+  (2,  'Smirnoff', 500,  '2022-02-10'),
+  (3,  'Ford',     2000, '2022-04-30'),
+  (4,  'Ivanov',   750,  '2022-04-13'),
+  (5,  'Deev',     250,  '2022-04-07'),
+  (6,  'Poirot',   150,  '2022-02-11'),
+  (7,  'Li',       175,  '2022-03-08'),
+  (8,  'Ericsson', 185,  '2022-02-23'),
+  (9,  'Muller',   250,  '2022-03-11'),
+  (10, 'Halder',   350,  '2022-01-28'),
+  (11, 'Trump',    380,  '2022-04-06'),
+  (12, 'Plato',    350,  '2022-03-19'),
+  (13, 'Gandi',    377,  '2022-01-09'),
+  (14, 'Smith',    510,  '2022-05-04');
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES
+  ('Manager1', 2021, 12, 7),
+  ('Manager2', 2021, 12, 8),
+  ('Manager3', 2022, 1, 1),
+  ('Manager1', 2022, 2, 4),
+  ('Manager2', 2022, 1, 2),
+  ('Manager3', 2022, 2, 1),
+  ('Manager1', 2022, 3, 3),
+  ('Manager2', 2022, 3, 4),
+  ('Manager3', 2022, 5, 1);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES
+  ('Manager1', 2022, 1, 10),
+  ('Manager2', 2022, 2, 10);
+INSERT INTO salespeople VALUES
+  (1, 'Poirot'),
+  (10, 'May'),
+  (30, 'Ford');
+INSERT INTO sales_range VALUES
+  (1,  'May',      1000, '2022-01-31'),
+  (2,  'Smirnoff', 500,  '2022-02-10'),
+  (3,  'Ford',     2000, '2022-04-30'),
+  (4,  'Ivanov',   750,  '2022-04-13'),
+  (5,  'Deev',     250,  '2022-04-07'),
+  (6,  'Poirot',   150,  '2022-02-11'),
+  (7,  'Li',       175,  '2022-03-08'),
+  (8,  'Ericsson', 185,  '2022-02-23'),
+  (9,  'Muller',   250,  '2022-03-11'),
+  (10, 'Halder',   350,  '2022-01-28'),
+  (11, 'Trump',    380,  '2022-04-06'),
+  (12, 'Plato',    350,  '2022-03-19'),
+  (13, 'Gandi',    377,  '2022-01-09'),
+  (14, 'Smith',    510,  '2022-05-04');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES
+  ('Trump',    'Bejing',         1000, '2022-03-01'),
+  ('Smirnoff', 'New York',        500, '2022-03-03'),
+  ('Ford',     'St. Petersburg', 2000, '2022-03-05'),
+  ('Ivanov',   'Warsaw',          750, '2022-03-04'),
+  ('Deev',     'Lisbon',          250, '2022-03-07'),
+  ('Poirot',   'Berlin',         1000, '2022-03-01'),
+  ('May',      'Helsinki',       1200, '2022-03-06'),
+  ('Li',       'Vladivostok',    1150, '2022-03-09'),
+  ('May',      'Helsinki',       1200, '2022-03-11'),
+  ('Halder',   'Oslo',            800, '2022-03-02'),
+  ('Muller',   'Madrid',          650, '2022-03-05'),
+  ('Smith',    'Kyiv',            350, '2022-03-10'),
+  ('Gandi',    'Warsaw',          150, '2022-03-08'),
+  ('Plato',    'Lisbon',          950, '2022-03-05');
+-- Test for hash partitioned table
+CREATE TABLE t (i int) PARTITION BY HASH(i);
+CREATE TABLE tp1 PARTITION OF t FOR VALUES WITH (MODULUS 2, REMAINDER 0);
+CREATE TABLE tp2 PARTITION OF t FOR VALUES WITH (MODULUS 2, REMAINDER 1);
+-- ERROR:  partition of hash-partitioned table cannot be merged
+ALTER TABLE t MERGE PARTITIONS (tp1, tp2) INTO tp3;
+-- ERROR:  list of new partitions should contain at least two items
+ALTER TABLE t MERGE PARTITIONS (tp1) INTO tp3;

From 8198249ebf4f4dc19fd0cfd6576d8ccbf3eb292b Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:58:09 +0300
Subject: [PATCH v40 2/2] Implement ALTER TABLE ... SPLIT PARTITION ... command

 src/include/nodes/parsenodes.h                |    3 +-
 src/test/regress/expected/partition_split.out | 1741 +++++++++++++++++
 src/test/regress/sql/partition_split.sql      | 1070 ++++++++++
 20 files changed, 4394 insertions(+), 18 deletions(-)

 

+  <para>
index b4ec6691d8..58e95c65fc 100644

 
 
+/*
+						errcode(ERRCODE_CHECK_VIOLATION),
+						errtable(splitRel));
 
+/*
+					errcode(ERRCODE_DUPLICATE_TABLE),
+					errmsg("relation \"%s\" already exists", relname));
index 47823ef7a5..750f531b81 100644

 
+partitions_list:
index 0d3ab35a57..510f854def 100644
 
+/*
+	splitPartOid = RangeVarGetRelid(partcmd->name, AccessShareLock, false);
@@ -3873,7 +3907,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3881,6 +3915,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
+			case AT_SplitPartition:
 			case AT_MergePartitions:
 				{
 					PartitionCmd *partcmd = (PartitionCmd *) cmd->def;
@@ -3889,7 +3924,11 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 						ereport(ERROR,
 								errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 								errmsg("list of new partitions should contain at least two items"));
-					transformPartitionCmdForMerge(&cxt, partcmd);
+
+					if (cmd->subtype == AT_SplitPartition)
+						transformPartitionCmdForSplit(&cxt, partcmd);
+					else
+						transformPartitionCmdForMerge(&cxt, partcmd);
 					newcmds = lappend(newcmds, cmd);
 					break;
 				}
@@ -4324,13 +4363,13 @@ setSchemaName(const char *context_schema, char **stmt_schema_name)
 
 /*
  * transformPartitionCmd
 
@@ -4339,9 +4378,9 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 
@@ -4349,7 +4388,7 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
index cb719f1ff5..42146c3bfa 100644
 
+/*
+						errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						parser_errposition(pstate, overlap_location));
 
+/*
+				errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				parser_errposition(pstate, datum->location));
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							parser_errposition(pstate, datum->location));
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							parser_errposition(pstate, datum->location));
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							parser_errposition(pstate, datum->location));
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							parser_errposition(pstate, datum->location));
+						errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						parser_errposition(pstate, overlap_location));
+					errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					parser_errposition(pstate, overlap_location));
+				errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				parser_errposition(pstate, overlap_location));
+				errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					   searchNull ? "NULL" : get_list_partvalue_string(notFoundVal)));
+									errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+									errmsg("DEFAULT partition should be one"),
+					errmsg("partition of hash-partitioned table cannot be split"));
+				errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				errmsg("one partition in the list should be DEFAULT because split partition is DEFAULT"),
+				errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				errmsg("new partition cannot be DEFAULT because DEFAULT partition already exists"),
+						errcode(ERRCODE_DUPLICATE_TABLE),
+						errmsg("name \"%s\" is already used", sps2->name->relname),
 
 	return buf->data;
 }
+
+/*
 
 	/*
 
index 230a42b966..bbd1110a40 100644
@@ -970,7 +970,7 @@ typedef struct SinglePartitionSpec
-	RangeVar   *name;			/* name of partition to attach/detach/merge */
+	RangeVar   *name;			/* name of partition to attach/detach/merge/split */

 
index 0000000000..51886c66b4
@@ -0,0 +1,1741 @@
+INSERT INTO sales_range VALUES
+  (1,  'May',      1000, '2022-01-31'),
+  (2,  'Smirnoff', 500,  '2022-02-10'),
+  (3,  'Ford',     2000, '2022-04-30'),
+  (4,  'Ivanov',   750,  '2022-04-13'),
+  (5,  'Deev',     250,  '2022-04-07'),
+  (6,  'Poirot',   150,  '2022-02-11'),
+  (7,  'Li',       175,  '2022-03-08'),
+  (8,  'Ericsson', 185,  '2022-02-23'),
+  (9,  'Muller',   250,  '2022-03-11'),
+  (10, 'Halder',   350,  '2022-01-28'),
+  (11, 'Trump',    380,  '2022-04-06'),
+  (12, 'Plato',    350,  '2022-03-19'),
+  (13, 'Gandi',    377,  '2022-01-09'),
+  (14, 'Smith',    510,  '2022-05-04');
+INSERT INTO sales_range VALUES
+  (1,  'May',      1000, '2022-01-31'),
+  (2,  'Smirnoff', 500,  '2022-02-10'),
+  (3,  'Ford',     2000, '2022-04-30'),
+  (4,  'Ivanov',   750,  '2022-04-13'),
+  (5,  'Deev',     250,  '2022-04-07'),
+  (6,  'Poirot',   150,  '2022-02-11'),
+  (7,  'Li',       175,  '2022-03-08'),
+  (8,  'Ericsson', 185,  '2022-02-23'),
+  (9,  'Muller',   250,  '2022-03-11'),
+  (10, 'Halder',   350,  '2022-01-28'),
+  (11, 'Trump',    380,  '2022-04-06'),
+  (12, 'Plato',    350,  '2022-03-19'),
+  (13, 'Gandi',    377,  '2022-01-09'),
+  (14, 'Smith',    510,  '2022-05-04');
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES
+  ('Manager1', 2021, 12, 7),
+  ('Manager2', 2021, 12, 8),
+  ('Manager3', 2022, 1, 1),
+  ('Manager1', 2022, 2, 4),
+  ('Manager2', 2022, 1, 2),
+  ('Manager3', 2022, 2, 1),
+  ('Manager1', 2022, 3, 3),
+  ('Manager2', 2022, 3, 4),
+  ('Manager3', 2022, 5, 1);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES
+  ('Manager1', 2022, 1, 10),
+  ('Manager2', 2022, 2, 10);
+INSERT INTO sales_range VALUES
+  (1,  'May',      1000, '2022-01-31'),
+  (2,  'Smirnoff', 500,  '2022-02-10'),
+  (3,  'Ford',     2000, '2022-04-30'),
+  (4,  'Ivanov',   750,  '2022-04-13'),
+  (5,  'Deev',     250,  '2022-04-07'),
+  (6,  'Poirot',   150,  '2022-02-11'),
+  (7,  'Li',       175,  '2022-03-08'),
+  (8,  'Ericsson', 185,  '2022-02-23'),
+  (9,  'Muller',   250,  '2022-03-11'),
+  (10, 'Halder',   350,  '2022-01-28'),
+  (11, 'Trump',    380,  '2022-04-06'),
+  (12, 'Plato',    350,  '2022-03-19'),
+  (13, 'Gandi',    377,  '2022-01-09'),
+  (14, 'Smith',    510,  '2022-05-04');
+INSERT INTO salespeople VALUES
+  (1,  'Poirot'),
+  (10, 'May'),
+  (19, 'Ivanov'),
+  (20, 'Smirnoff'),
+  (30, 'Ford');
+INSERT INTO sales VALUES
+  (1,  100, '2022-03-01'),
+  (1,  110, '2022-03-02'),
+  (10, 150, '2022-03-01'),
+  (10, 90,  '2022-03-03'),
+  (19, 200, '2022-03-04'),
+  (20, 50,  '2022-03-12'),
+  (20, 170, '2022-03-02'),
+  (30, 30,  '2022-03-04');
+INSERT INTO salespeople (salesperson_name) VALUES ('Poirot'), ('Ivanov');
+INSERT INTO salespeople (salesperson_name) VALUES ('May'), ('Ford');
+INSERT INTO salespeople10_40 VALUES
+  ('dummy value 1', 19, 100, now(), 'Ivanov'),
+  ('dummy value 2', 20, 101, now(), 'Smirnoff');
+INSERT INTO salespeople VALUES
+  (1, 'Poirot'),
+  (10, 'May'),
+  (30, 'Ford');
+INSERT INTO sales_range VALUES
+  (1,  'May',      1000, '2022-01-31'),
+  (2,  'Smirnoff', 500,  '2022-02-10'),
+  (3,  'Ford',     2000, '2022-04-30'),
+  (4,  'Ivanov',   750,  '2022-04-13'),
+  (5,  'Deev',     250,  '2022-04-07'),
+  (6,  'Poirot',   150,  '2022-02-11'),
+  (7,  'Li',       175,  '2022-03-08'),
+  (8,  'Ericsson', 185,  '2022-02-23'),
+  (9,  'Muller',   250,  '2022-03-11'),
+  (10, 'Halder',   350,  '2022-01-28'),
+  (11, 'Trump',    380,  '2022-04-06'),
+  (12, 'Plato',    350,  '2022-03-19'),
+  (13, 'Gandi',    377,  '2022-01-09'),
+  (14, 'Smith',    510,  '2022-05-04');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES
+  ('Trump',    'Bejing',         1000, '2022-03-01'),
+  ('Smirnoff', 'New York',        500, '2022-03-03'),
+  ('Ford',     'St. Petersburg', 2000, '2022-03-05'),
+  ('Ivanov',   'Warsaw',          750, '2022-03-04'),
+  ('Deev',     'Lisbon',          250, '2022-03-07'),
+  ('Poirot',   'Berlin',         1000, '2022-03-01'),
+  ('May',      'Oslo',           1200, '2022-03-06'),
+  ('Li',       'Vladivostok',    1150, '2022-03-09'),
+  ('May',      'Oslo',           1200, '2022-03-11'),
+  ('Halder',   'Helsinki',        800, '2022-03-02'),
+  ('Muller',   'Madrid',          650, '2022-03-05'),
+  ('Smith',    'Kyiv',            350, '2022-03-10'),
+  ('Gandi',    'Warsaw',          150, '2022-03-08'),
+  ('Plato',    'Lisbon',          950, '2022-03-05');
+INSERT INTO sales_range VALUES
+  (1,  'May',      1000, '2022-01-31'),
+  (2,  'Smirnoff', 500,  '2022-02-09'),
+  (3,  'Ford',     2000, '2022-04-30'),
+  (4,  'Ivanov',   750,  '2022-04-13'),
+  (5,  'Deev',     250,  '2022-04-07'),
+  (6,  'Poirot',   150,  '2022-02-07'),
+  (7,  'Li',       175,  '2022-03-08'),
+  (8,  'Ericsson', 185,  '2022-02-23'),
+  (9,  'Muller',   250,  '2022-03-11'),
+  (10, 'Halder',   350,  '2022-01-28'),
+  (11, 'Trump',    380,  '2022-04-06'),
+  (12, 'Plato',    350,  '2022-03-19'),
+  (13, 'Gandi',    377,  '2022-01-09'),
+  (14, 'Smith',    510,  '2022-05-04');
+INSERT INTO sales_range VALUES
+  (1,  'May',      1000, '2022-01-31'),
+  (2,  'Smirnoff', 500,  '2022-02-09'),
+  (3,  'Ford',     2000, '2022-04-30'),
+  (4,  'Ivanov',   750,  '2022-04-13'),
+  (5,  'Deev',     250,  '2022-04-07'),
+  (6,  'Poirot',   150,  '2022-02-07'),
+  (7,  'Li',       175,  '2022-03-08'),
+  (8,  'Ericsson', 185,  '2022-02-23'),
+  (9,  'Muller',   250,  '2022-03-11'),
+  (10, 'Halder',   350,  '2022-01-28'),
+  (11, 'Trump',    380,  '2022-04-06'),
+  (12, 'Plato',    350,  '2022-03-19'),
+  (13, 'Gandi',    377,  '2022-01-09'),
+  (14, 'Smith',    510,  '2022-05-04');
+INSERT INTO sales_range VALUES
+  (1,  'May',      1000, '2022-01-31'),
+  (2,  'Smirnoff', 500,  '2022-02-10'),
+  (3,  'Ford',     2000, '2022-04-30'),
+  (4,  'Ivanov',   750,  '2022-04-13'),
+  (5,  'Deev',     250,  '2022-04-07'),
+  (6,  'Poirot',   150,  '2022-02-11'),
+  (7,  'Li',       175,  '2022-03-08'),
+  (8,  'Ericsson', 185,  '2022-02-23'),
+  (9,  'Muller',   250,  '2022-03-11'),
+  (10, 'Halder',   350,  '2022-01-28'),
+  (11, 'Trump',    380,  '2022-04-06'),
+  (12, 'Plato',    350,  '2022-03-19'),
+  (13, 'Gandi',    377,  '2022-01-09');
+-- Test for hash partitioned table
+CREATE TABLE t (i int) PARTITION BY HASH(i);
+CREATE TABLE tp1 PARTITION OF t FOR VALUES WITH (MODULUS 2, REMAINDER 0);
+CREATE TABLE tp2 PARTITION OF t FOR VALUES WITH (MODULUS 2, REMAINDER 1);
+-- ERROR:  partition of hash-partitioned table cannot be split
+ALTER TABLE t SPLIT PARTITION tp1 INTO
+  (PARTITION tp1_1 FOR VALUES WITH (MODULUS 4, REMAINDER 0),
+   PARTITION tp1_2 FOR VALUES WITH (MODULUS 4, REMAINDER 2));
+ERROR:  partition of hash-partitioned table cannot be split
+-- ERROR:  list of new partitions should contain at least two items
+ALTER TABLE t SPLIT PARTITION tp1 INTO
+  (PARTITION tp1_1 FOR VALUES WITH (MODULUS 4, REMAINDER 0));
 
index 0000000000..682a83793a
@@ -0,0 +1,1070 @@
+INSERT INTO sales_range VALUES
+  (1,  'May',      1000, '2022-01-31'),
+  (2,  'Smirnoff', 500,  '2022-02-10'),
+  (3,  'Ford',     2000, '2022-04-30'),
+  (4,  'Ivanov',   750,  '2022-04-13'),
+  (5,  'Deev',     250,  '2022-04-07'),
+  (6,  'Poirot',   150,  '2022-02-11'),
+  (7,  'Li',       175,  '2022-03-08'),
+  (8,  'Ericsson', 185,  '2022-02-23'),
+  (9,  'Muller',   250,  '2022-03-11'),
+  (10, 'Halder',   350,  '2022-01-28'),
+  (11, 'Trump',    380,  '2022-04-06'),
+  (12, 'Plato',    350,  '2022-03-19'),
+  (13, 'Gandi',    377,  '2022-01-09'),
+  (14, 'Smith',    510,  '2022-05-04');
+INSERT INTO sales_range VALUES
+  (1,  'May',      1000, '2022-01-31'),
+  (2,  'Smirnoff', 500,  '2022-02-10'),
+  (3,  'Ford',     2000, '2022-04-30'),
+  (4,  'Ivanov',   750,  '2022-04-13'),
+  (5,  'Deev',     250,  '2022-04-07'),
+  (6,  'Poirot',   150,  '2022-02-11'),
+  (7,  'Li',       175,  '2022-03-08'),
+  (8,  'Ericsson', 185,  '2022-02-23'),
+  (9,  'Muller',   250,  '2022-03-11'),
+  (10, 'Halder',   350,  '2022-01-28'),
+  (11, 'Trump',    380,  '2022-04-06'),
+  (12, 'Plato',    350,  '2022-03-19'),
+  (13, 'Gandi',    377,  '2022-01-09'),
+  (14, 'Smith',    510,  '2022-05-04');
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES
+  ('Manager1', 2021, 12, 7),
+  ('Manager2', 2021, 12, 8),
+  ('Manager3', 2022, 1, 1),
+  ('Manager1', 2022, 2, 4),
+  ('Manager2', 2022, 1, 2),
+  ('Manager3', 2022, 2, 1),
+  ('Manager1', 2022, 3, 3),
+  ('Manager2', 2022, 3, 4),
+  ('Manager3', 2022, 5, 1);
+INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES
+  ('Manager1', 2022, 1, 10),
+  ('Manager2', 2022, 2, 10);
+INSERT INTO sales_range VALUES
+  (1,  'May',      1000, '2022-01-31'),
+  (2,  'Smirnoff', 500,  '2022-02-10'),
+  (3,  'Ford',     2000, '2022-04-30'),
+  (4,  'Ivanov',   750,  '2022-04-13'),
+  (5,  'Deev',     250,  '2022-04-07'),
+  (6,  'Poirot',   150,  '2022-02-11'),
+  (7,  'Li',       175,  '2022-03-08'),
+  (8,  'Ericsson', 185,  '2022-02-23'),
+  (9,  'Muller',   250,  '2022-03-11'),
+  (10, 'Halder',   350,  '2022-01-28'),
+  (11, 'Trump',    380,  '2022-04-06'),
+  (12, 'Plato',    350,  '2022-03-19'),
+  (13, 'Gandi',    377,  '2022-01-09'),
+  (14, 'Smith',    510,  '2022-05-04');
+INSERT INTO salespeople VALUES
+  (1,  'Poirot'),
+  (10, 'May'),
+  (19, 'Ivanov'),
+  (20, 'Smirnoff'),
+  (30, 'Ford');
+INSERT INTO sales VALUES
+  (1,  100, '2022-03-01'),
+  (1,  110, '2022-03-02'),
+  (10, 150, '2022-03-01'),
+  (10, 90,  '2022-03-03'),
+  (19, 200, '2022-03-04'),
+  (20, 50,  '2022-03-12'),
+  (20, 170, '2022-03-02'),
+  (30, 30,  '2022-03-04');
+INSERT INTO salespeople (salesperson_name) VALUES ('Poirot'), ('Ivanov');
+INSERT INTO salespeople (salesperson_name) VALUES ('May'), ('Ford');
+INSERT INTO salespeople10_40 VALUES
+  ('dummy value 1', 19, 100, now(), 'Ivanov'),
+  ('dummy value 2', 20, 101, now(), 'Smirnoff');
+INSERT INTO salespeople VALUES
+  (1, 'Poirot'),
+  (10, 'May'),
+  (30, 'Ford');
+INSERT INTO sales_range VALUES
+  (1,  'May',      1000, '2022-01-31'),
+  (2,  'Smirnoff', 500,  '2022-02-10'),
+  (3,  'Ford',     2000, '2022-04-30'),
+  (4,  'Ivanov',   750,  '2022-04-13'),
+  (5,  'Deev',     250,  '2022-04-07'),
+  (6,  'Poirot',   150,  '2022-02-11'),
+  (7,  'Li',       175,  '2022-03-08'),
+  (8,  'Ericsson', 185,  '2022-02-23'),
+  (9,  'Muller',   250,  '2022-03-11'),
+  (10, 'Halder',   350,  '2022-01-28'),
+  (11, 'Trump',    380,  '2022-04-06'),
+  (12, 'Plato',    350,  '2022-03-19'),
+  (13, 'Gandi',    377,  '2022-01-09'),
+  (14, 'Smith',    510,  '2022-05-04');
+INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES
+  ('Trump',    'Bejing',         1000, '2022-03-01'),
+  ('Smirnoff', 'New York',        500, '2022-03-03'),
+  ('Ford',     'St. Petersburg', 2000, '2022-03-05'),
+  ('Ivanov',   'Warsaw',          750, '2022-03-04'),
+  ('Deev',     'Lisbon',          250, '2022-03-07'),
+  ('Poirot',   'Berlin',         1000, '2022-03-01'),
+  ('May',      'Oslo',           1200, '2022-03-06'),
+  ('Li',       'Vladivostok',    1150, '2022-03-09'),
+  ('May',      'Oslo',           1200, '2022-03-11'),
+  ('Halder',   'Helsinki',        800, '2022-03-02'),
+  ('Muller',   'Madrid',          650, '2022-03-05'),
+  ('Smith',    'Kyiv',            350, '2022-03-10'),
+  ('Gandi',    'Warsaw',          150, '2022-03-08'),
+  ('Plato',    'Lisbon',          950, '2022-03-05');
+INSERT INTO sales_range VALUES
+  (1,  'May',      1000, '2022-01-31'),
+  (2,  'Smirnoff', 500,  '2022-02-09'),
+  (3,  'Ford',     2000, '2022-04-30'),
+  (4,  'Ivanov',   750,  '2022-04-13'),
+  (5,  'Deev',     250,  '2022-04-07'),
+  (6,  'Poirot',   150,  '2022-02-07'),
+  (7,  'Li',       175,  '2022-03-08'),
+  (8,  'Ericsson', 185,  '2022-02-23'),
+  (9,  'Muller',   250,  '2022-03-11'),
+  (10, 'Halder',   350,  '2022-01-28'),
+  (11, 'Trump',    380,  '2022-04-06'),
+  (12, 'Plato',    350,  '2022-03-19'),
+  (13, 'Gandi',    377,  '2022-01-09'),
+  (14, 'Smith',    510,  '2022-05-04');
+INSERT INTO sales_range VALUES
+  (1,  'May',      1000, '2022-01-31'),
+  (2,  'Smirnoff', 500,  '2022-02-09'),
+  (3,  'Ford',     2000, '2022-04-30'),
+  (4,  'Ivanov',   750,  '2022-04-13'),
+  (5,  'Deev',     250,  '2022-04-07'),
+  (6,  'Poirot',   150,  '2022-02-07'),
+  (7,  'Li',       175,  '2022-03-08'),
+  (8,  'Ericsson', 185,  '2022-02-23'),
+  (9,  'Muller',   250,  '2022-03-11'),
+  (10, 'Halder',   350,  '2022-01-28'),
+  (11, 'Trump',    380,  '2022-04-06'),
+  (12, 'Plato',    350,  '2022-03-19'),
+  (13, 'Gandi',    377,  '2022-01-09'),
+  (14, 'Smith',    510,  '2022-05-04');
+INSERT INTO sales_range VALUES
+  (1,  'May',      1000, '2022-01-31'),
+  (2,  'Smirnoff', 500,  '2022-02-10'),
+  (3,  'Ford',     2000, '2022-04-30'),
+  (4,  'Ivanov',   750,  '2022-04-13'),
+  (5,  'Deev',     250,  '2022-04-07'),
+  (6,  'Poirot',   150,  '2022-02-11'),
+  (7,  'Li',       175,  '2022-03-08'),
+  (8,  'Ericsson', 185,  '2022-02-23'),
+  (9,  'Muller',   250,  '2022-03-11'),
+  (10, 'Halder',   350,  '2022-01-28'),
+  (11, 'Trump',    380,  '2022-04-06'),
+  (12, 'Plato',    350,  '2022-03-19'),
+  (13, 'Gandi',    377,  '2022-01-09');
+-- Test for hash partitioned table
+CREATE TABLE t (i int) PARTITION BY HASH(i);
+CREATE TABLE tp1 PARTITION OF t FOR VALUES WITH (MODULUS 2, REMAINDER 0);
+CREATE TABLE tp2 PARTITION OF t FOR VALUES WITH (MODULUS 2, REMAINDER 1);
+-- ERROR:  partition of hash-partitioned table cannot be split
+ALTER TABLE t SPLIT PARTITION tp1 INTO
+  (PARTITION tp1_1 FOR VALUES WITH (MODULUS 4, REMAINDER 0),
+   PARTITION tp1_2 FOR VALUES WITH (MODULUS 4, REMAINDER 2));
+-- ERROR:  list of new partitions should contain at least two items
+ALTER TABLE t SPLIT PARTITION tp1 INTO
+  (PARTITION tp1_1 FOR VALUES WITH (MODULUS 4, REMAINDER 0));

---

From: Alexander Korotkov <[email-masked]>
Date: Thu, 5 Jun 2025 04:41:47 +0300

Hi Dmitry!

> Thank you very much for review.

Thank you for your work on this patch.  I have some additional notes on
this patch.

Why don't you use *existing_relation_id argument of
RangeVarGetAndCheckCreationNamespace(), when it is called from
createPartitionTable() and ATExecSplitPartition()?  This argument provide
an elegant way to find a duplicate table with the same name.

It also seems that 0002 patch has the following error message, which aren't
experienced in the regression tests.

+                   datum = list_nth(spec->upperdatums, abs(cmpval) - 1);
+                   ereport(ERROR,
+                           errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                           errmsg("upper bound of partition \"%s\" is not
equal to upper bound of split partition",
+                                  relname),
+                           parser_errposition(pstate, datum->location));

+               ereport(ERROR,
+                       errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                       errmsg("new partition \"%s\" cannot have this value
because split partition does not have",
+                              relname),
+                       parser_errposition(pstate, overlap_location));

+       ereport(ERROR,
+               errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+               errmsg("new partitions do not have value %s but split
partition does",
+                      searchNull ? "NULL" :
get_list_partvalue_string(notFoundVal)));

+                           ereport(ERROR,
+
errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                                   errmsg("DEFAULT partition should be
one"),
+                                   parser_errposition(pstate,
sps->name->location));

+       ereport(ERROR,
+               errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+               errmsg("one partition in the list should be DEFAULT because
split partition is DEFAULT"),
+               parser_errposition(pstate, ((SinglePartitionSpec *)
linitial(partlist))->name->location));

+       ereport(ERROR,
+               errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+               errmsg("new partition cannot be DEFAULT because DEFAULT
partition already exists"),
+               parser_errposition(pstate, spsDef->name->location));

+               ereport(ERROR,
+                       errcode(ERRCODE_CHECK_VIOLATION),
+                       errmsg("can not find partition for split partition
row"),
+                       errtable(splitRel));

------
Regards,
Alexander Korotkov
Supabase

---

From: jian he <[email-masked]>
Date: Thu, 5 Jun 2025 12:22:40 +0800

hi.

the following are review of
v40-0001-Implement-ALTER-TABLE-.-MERGE-PARTITIONS-.-comma.patch

ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022,
sales_mar2022) INTO sales_feb_mar_apr2022;
There are no tests when sales_feb2022 or sales_mar2022 have any constraints.
a partition can have its own constraint.

What should we do when any to be merged partition has constraints?
----------------------------------------------------------------
DROP TABLE IF EXISTS sales_range cascade;
CREATE TABLE sales_range (salesperson_id INT, salesperson_name text,
sales_amount INT generated always as (1) stored, sales_date DATE)
PARTITION BY RANGE (sales_date);
CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM
('2022-02-01') TO ('2022-03-01');
CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM
('2022-03-01') TO ('2022-04-01');
ALTER TABLE sales_feb2022 ALTER COLUMN sales_amount SET EXPRESSION AS (10);
ALTER TABLE sales_mar2022 ALTER COLUMN sales_amount SET EXPRESSION AS (20);

ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022,
sales_mar2022) INTO sales_feb_mar2022;
with v40, sales_feb_mar2022 column sales_int generated expression is
(generated always as (1) stored)

maybe this is what we expected.
but we should have some tests on it.
----------------------------------------------------------------
DROP TABLE IF EXISTS sales_range cascade;
CREATE TABLE sales_range (salesperson_id INT, salesperson_name text,
sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date);
CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM
('2022-02-01') TO ('2022-03-01');
CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM
('2022-03-01') TO ('2022-04-01');

CREATE VIEW x AS SELECT * FROM sales_mar2022;
ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022,
sales_mar2022) INTO sales_feb_mar2022;
ERROR:  cannot drop table public.sales_mar2022 because other objects
depend on it
DETAIL:  view public.x depends on table public.sales_mar2022
HINT:  Use DROP ... CASCADE to drop the dependent objects too.

Maybe this is expected, but we need to mention it somewhere and have
some tests on it.
saying that MERGE PARTITIONS will effectively drop the partitions, so
if any object depends on that partition
then MERGE PARTITIONS can not be done.
----------------------------------------------------------------
+ */
+static Relation
+createPartitionTable(RangeVar *newPartName, Relation modelRel, Oid ownerId)
+{
+ Relation newRel;
+ Oid newRelId;
+ TupleDesc descriptor;
+ List   *colList = NIL;
+ Oid relamId;
+ Oid namespaceId;
+
+ /* If existing rel is temp, it must belong to this session */
+ if (modelRel->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
+ !modelRel->rd_islocaltemp)
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot create as partition of temporary relation of another
session"));

Looking at it, modelRel is the partitioned table we called ALTER TABLE.
for example:
ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022,
sales_mar2022) INTO sales_feb_mar2022;
modelRel is sales_range.

so this error check can be performed as early as the
transformPartitionCmdForMerge stage?
----------------------------------------------------------------
+ /* Look up the access method for new relation. */
+ relamId = (modelRel->rd_rel->relam != InvalidOid) ?
modelRel->rd_rel->relam : HEAP_TABLE_AM_OID;
looking at the output of "select * from pg_am;".

i think, we can do the following way:
if (modelRel->rd_rel->relam)
elog(ERROR, "error");
relamId = modelRel->rd_rel->relam;
----------------------------------------------------------------
Attached is some refactoring in moveMergedTablesRows, hope it's straightforward.

for example:
execute_attr_map_slot will call "slot_getallattrs(srcslot);" so the
first one is unncessary.

+ srcslot = MakeSingleTupleTableSlot(RelationGetDescr(mergingPartition),
+   table_slot_callbacks(mergingPartition));
can change to
srcslot = table_slot_create(mergingPartition, NULL);

---

From: jian he <[email-masked]>
Date: Thu, 5 Jun 2025 15:16:34 +0800

hi.
bug in transformPartitionCmdForMerge "equal(name, name2))"

+static void

ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022,
public.sales_feb2022) INTO sales_feb_mar2022;
ERROR:  lower bound of partition "sales_feb2022" conflicts with upper
bound of previous partition "sales_feb2022"
in this context. "sales_feb2022" is the same as "public.sales_feb2022".

---

From: jian he <[email-masked]>
Date: Thu, 5 Jun 2025 20:24:42 +0800

hi.

When using ALTER TABLE ... MERGE PARTITIONS, some of the new
partition's properties will
not be inherited from to be merged partitions; instead, they will be directly
copied from the root partitioned table.
so we need to test this behavior.

The attached test file is for test table properties:
(COMMENTS, COMPRESSION, DEFAULTS, GENERATED, STATISTICS, STORAGE).

STATISTICS: to be merged partition's STATISTICS will be dropped.
COMMENTS: to be merged partition's COMMENTS will be dropped.

---

From: Dmitry Koval <[email-masked]>
Date: Thu, 5 Jun 2025 19:40:12 +0300

Hi Alexander!
Thanks for your notes!

1.
 >createPartitionTable() and ATExecSplitPartition()?  This argument
 >provide an elegant way to find a duplicate table with the same name.

Code changed.

2.
 >It also seems that 0002 patch has the following error message, which
 >aren't experienced in the regression tests.

2a. Added tests for these error messages:
+errmsg("upper bound of partition \"%s\" is not equal to upper bound of 
split partition",
+errmsg("new partition \"%s\" cannot have this value because split 
partition does not have",
+errmsg("DEFAULT partition should be one"),
+errmsg("new partition cannot be DEFAULT because DEFAULT partition 
already exists"),

2b. Tests for these error messages already exists:
+errmsg("new partitions do not have value %s but split partition does",
+errmsg("one partition in the list should be DEFAULT because split 
partition is DEFAULT"),

2c. The error message
+errmsg("can not find partition for split partition row"),
cannot be reproduced using regression tests, because it is issued when
partition contains a record that should not be there (i.e. when the
database is corrupted).

-- 
With best regards,
Dmitry Koval

Postgres Professional: http://postgrespro.com
From 502ee25987a52ba02a248136a66157d5d9ddc42f Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:57:22 +0300
Subject: [PATCH v41 1/2] Implement ALTER TABLE ... MERGE PARTITIONS ...
 command

 src/backend/commands/tablecmds.c              |  655 ++++++++++-
 20 files changed, 3070 insertions(+), 28 deletions(-)

 

   <para>
 
+  <para>
 
  <refsect1>
index 164ec56a59..5579b0ce01 100644

 
@@ -20166,6 +20187,37 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
 	}
 }
 
+/*
@@ -20369,23 +20421,8 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -22032,3 +22069,587 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+		RangeVarGetAndCheckCreationNamespace(newPartName, NoLock, &existingRelid);
+				errmsg("relation \"%s\" already exists", newPartName->relname));
+	 * Look up existing relation by new partition name, check we have
+	 * permission to create there, lock it against concurrent drop, and mark
+	 * stmt->relation as RELPERSISTENCE_TEMP if a temporary namespace is
+	 * selected.
+	RangeVarGetAndCheckCreationNamespace(cmd->name, NoLock, &existingRelid);

 

+/*
+ * checkPartition

 				/*
 
 						/*

 

+/*

 

From 1c1a37a268019739682dff33cc86fa47a5a61459 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:58:09 +0300
Subject: [PATCH v41 2/2] Implement ALTER TABLE ... SPLIT PARTITION ... command

 src/backend/commands/tablecmds.c              |  419 ++++
 src/test/regress/expected/partition_split.out | 1781 +++++++++++++++++
 src/test/regress/sql/partition_split.sql      | 1106 ++++++++++
 20 files changed, 4467 insertions(+), 18 deletions(-)

 

+  <para>
index 5579b0ce01..2861b8d742 100644

 
@@ -22070,6 +22092,257 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 	return cstorage;
 }
 
+/*
@@ -22378,6 +22651,152 @@ createPartitionTable(RangeVar *newPartName, Relation modelRel, Oid ownerId)
 	return newRel;
 }
 
+/*
+		 * Look up existing relation by new partition name, check we have
+		 * permission to create there, lock it against concurrent drop, and mark
+		 * stmt->relation as RELPERSISTENCE_TEMP if a temporary namespace is
+		 * selected.
+		RangeVarGetAndCheckCreationNamespace(sps->name, NoLock, &existingRelid);
+		if (existingRelid == splitRelOid && !isSameName)
+		else if (OidIsValid(existingRelid))

 
+partitions_list:
 
+/*

 
 /*
  * transformPartitionCmd

 
+/*
 
+/*
 
 	return buf->data;
 }
+
+/*
 
 	/*

 
 
index 0000000000..9d471870e4
@@ -0,0 +1,1781 @@
+-- ERROR:  one partition in the list should be DEFAULT because split partition is DEFAULT
+-- ERROR:  new partition "sales_west" cannot have this value because split partition does not have
+  (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid', 'Melbourne'),
+ERROR:  new partition "sales_west" cannot have this value because split partition does not have
+LINE 2: ...st FOR VALUES IN ('Lisbon', 'New York', 'Madrid', 'Melbourne...
+-- ERROR:  new partition cannot be DEFAULT because DEFAULT partition already exists
+  (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid', 'Melbourne'),
+   PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'),
+   PARTITION sales_others2 DEFAULT);
+ERROR:  new partition cannot be DEFAULT because DEFAULT partition already exists
+LINE 5:    PARTITION sales_others2 DEFAULT);
+-- ERROR  DEFAULT partition should be one
+   PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'),
+   PARTITION sales_others DEFAULT,
+   PARTITION sales_others2 DEFAULT);
+ERROR:  DEFAULT partition should be one
+LINE 6:    PARTITION sales_others2 DEFAULT);
+-- Additional tests for error messages
+-- ERROR:  upper bound of partition "sales_apr2022" is not equal to upper bound of split partition
+ERROR:  upper bound of partition "sales_apr2022" is not equal to upper bound of split partition
 
index 0000000000..1f7b592ba0
@@ -0,0 +1,1106 @@
+-- ERROR:  one partition in the list should be DEFAULT because split partition is DEFAULT
+-- ERROR:  new partition "sales_west" cannot have this value because split partition does not have
+  (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid', 'Melbourne'),
+-- ERROR:  new partition cannot be DEFAULT because DEFAULT partition already exists
+  (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid', 'Melbourne'),
+   PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'),
+   PARTITION sales_others2 DEFAULT);
+-- ERROR  DEFAULT partition should be one
+   PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'),
+   PARTITION sales_others DEFAULT,
+   PARTITION sales_others2 DEFAULT);
+-- Additional tests for error messages
+-- ERROR:  upper bound of partition "sales_apr2022" is not equal to upper bound of split partition

---

From: Dmitry Koval <[email-masked]>
Date: Thu, 5 Jun 2025 19:41:22 +0300

Hi, jian he!

Thank you very much for your emails!
Unfortunately, due to urgent tasks at my work, I do not have time to 
look through your notes today and tomorrow.
But I will definitely do it at the beginning of next week.

-- 
With best regards,
Dmitry Koval

---

From: jian he <[email-masked]>
Date: Fri, 6 Jun 2025 10:28:00 +0800

hi.
one more patch for regress tests.

ALTER TABLE salespeople MERGE PARTITIONS (salespeople10_20,
salespeople20_30, salespeople30_40) INTO salespeople10_40;
the trigger on the merged partition  will be dropped.
For example, here, trigger on salespeople10_20 will be dropped.

I am surprised that partition_merge.sql doesn't have much \d+ command.
so I added two, which is necessary IMHO.

---

From: jian he <[email-masked]>
Date: Fri, 6 Jun 2025 15:13:22 +0800

hi.

in createTableConstraints
+ /* Add a pre-cooked default expression. */
+ StoreAttrDefault(newRel, num, def, true);
+
+ /* Store CHECK constraints. */
+ StoreConstraints(newRel, cookedConstraints, false);
Here, StoreConstraints last argument should be set to true?
see also StoreAttrDefault.

+static void
+ elog(ERROR, "default expression not found for attribute %d of
relation \"%s\"",
you can use TupleDescGetDefault, build_generation_expression
to simplify the above code.

The attached patch fixes the above issues.
it is based on v41-0001-Implement-ALTER-TABLE-.-MERGE-PARTITIONS-.-comma.patch
----------------------
Do getAttributesList need to care about pg_attribute.attidentity?
currently MERGE PARTITION seems to work fine with identity columns,
this issue i didn't dig deeper.

I am wondering right after createPartitionTable,
do we need a CommandCounterIncrement?
because later moveMergedTablesRows will use the output of createPartitionTable.

---

From: Dmitry Koval <[email-masked]>
Date: Tue, 10 Jun 2025 01:48:33 +0300

Hi, Jian He!

Thanks for the suggestions and patches!
This email contains comments to three emails (05/06/2025).
I hope to read two emails (for 06/06/2025) tomorrow.

1.
 >some tests on it saying that MERGE PARTITIONS will effectively drop
 >the partitions, so if any object depends on that partition

Added following phrases to the documentation (I hope this should be 
enough?):

If merged partitions have individual constraints, those constraints will 
be dropped because command uses partitioned table as a model to create 
the constraints.
If merged partitions have some objects dependent on them, the command 
can not be done (CASCADE is not used, an error will be returned).

2.
 > ... so this error check can be performed as early as the

Function createPartitionTable will be used for various other cases 
besides MERGE PARTITIONS: for SPLIT PARTITION, for PARTITION BY 
REFERENCE (I hope).
So I think it's better to minimize the amount of code and not move the 
same one check into different functions (transformPartitionCmdForMerge, 
transformPartitionCmdForSplit, ...).

3.
 >relamId = modelRel->rd_rel->relam;

Can you clarify what is reason to change the current AM-logic for 
creating a new partition?

(If AM is set for a partitioned table, then use it, otherwise use AM for 
heap tables.)

4.
 > Attached is some refactoring in moveMergedTablesRows, hope it's 
straightforward.

Thanks, these changes are useful.

5.
 >in this context. "sales_feb2022" is the same as "public.sales_feb2022".

Added check and test for this case.

6.
 >partition's properties will not be inherited from to be merged
 >partitions; instead, they will be directly copied from the root
 >partitioned table.
 >So we need to test this behavior.

Some tests already exist (GENERATED, DEFAULTS) - see 
partition_merge.sql, lines after:

+-- Test for:

But the complex test is probably also interesting.
Test added.

--

Similar changes are made for the second commit (SPLIT PARTITION).

-- 
With best regards,
Dmitry Koval

Postgres Professional: http://postgrespro.com
From c640bf22a681c4f9c12482fd4fb8c0efcd976a8d Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:57:22 +0300
Subject: [PATCH v42 1/2] Implement ALTER TABLE ... MERGE PARTITIONS ...
 command

Discussion: https://postgr.es/m/c73a1746-0cd0-6bdd-6b23-3ae0b7c0c582%40postgrespro.ru
Author: Dmitry Koval
Reviewed-by: Matthias van de Meent, Laurenz Albe, Zhihong Yu, Justin Pryzby
Reviewed-by: Alvaro Herrera, Robert Haas, Stephane Tachoires, Jian He

Fixes (summary information).
Authors: Alexander Korotkov, Tender Wang, Richard Guo, Dagfinn Ilmari Mannsaker
Authors: Fujii Masao
Reviewed-by: Alexander Korotkov, Robert Haas, Justin Pryzby, Pavel Borisov
Reviewed-by: Masahiko Sawada
Reported-by: Alexander Lakhin, Justin Pryzby, Kyotaro Horiguchi
Reported-by: Daniel Gustafsson, Tom Lane, Noah Misch
---
 doc/src/sgml/ddl.sgml                         |   19 +
 doc/src/sgml/ref/alter_table.sgml             |  105 +-
 src/backend/catalog/heap.c                    |    4 +-
 src/backend/commands/tablecmds.c              |  651 +++++++++-
 src/backend/parser/gram.y                     |   22 +-
 src/backend/parser/parse_utilcmd.c            |  144 +++
 src/backend/partitioning/partbounds.c         |  212 +++-
 src/bin/psql/tab-complete.in.c                |   10 +
 src/include/catalog/heap.h                    |    3 +
 src/include/nodes/parsenodes.h                |    5 +-
 src/include/parser/kwlist.h                   |    1 +
 src/include/partitioning/partbounds.h         |    6 +
 .../isolation/expected/partition-merge.out    |  199 +++
 src/test/isolation/isolation_schedule         |    1 +
 src/test/isolation/specs/partition-merge.spec |   54 +
 .../test_ddl_deparse/test_ddl_deparse.c       |    3 +
 src/test/regress/expected/partition_merge.out | 1068 +++++++++++++++++
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/partition_merge.sql      |  708 +++++++++++
 19 files changed, 3189 insertions(+), 28 deletions(-)
 create mode 100644 src/test/isolation/expected/partition-merge.out
 create mode 100644 src/test/isolation/specs/partition-merge.spec
 create mode 100644 src/test/regress/expected/partition_merge.out
 create mode 100644 src/test/regress/sql/partition_merge.sql

index d63f3a621a..8201efac86 100644

@@ -1147,14 +1150,102 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+      Hash-partitioning is not supported.  If <literal>DEFAULT</literal> partition is not in the
+      If <literal>DEFAULT</literal> partition is in the list of partitions <replaceable class="parameter">partition_name1</replaceable>,
+         will be the <literal>DEFAULT</literal> partition.
+      If merged partitions have individual constraints, those constraints will
+      be dropped because command uses partitioned table as a model to create
+      the constraints.
+      If merged partitions have some objects dependent on them, the command can
+      not be done (<literal>CASCADE</literal> is not used, an error will be returned).
 
   <para>
@@ -1397,7 +1488,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1830,6 +1922,13 @@ ALTER TABLE measurement
 
+  <para>
 
  <refsect1>
index ea96947d81..d6ce87dd96 100644

 
@@ -20169,6 +20190,37 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
 	}
 }
 
+/*
@@ -20372,23 +20424,8 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -22035,3 +22072,583 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+	dstslot = table_slot_create(newPartRel, NULL);
+		srcslot = table_slot_create(mergingPartition, NULL);

 
 
index 62015431fd..7815e8d245 100644
@@ -3488,6 +3491,134 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
 }

+/*
+ * checkPartition
+		 * Extended check because the same partition can have different names
+		 * (for example, "part_name" and "public.part_name").
+		foreach(listptr2, partOids)
+			Oid		curOid = lfirst_oid(listptr2);
+			if (curOid == partOid)
+						parser_errposition(cxt->pstate, name->location));
@@ -3765,6 +3896,19 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,

 				/*
 
 						/*

 
 
 extern void StoreAttrMissingVal(Relation rel, AttrNumber attnum,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index dd00ab420b..1ff0e7f6f0 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -959,8 +959,10 @@ typedef struct PartitionRangeDatum
 typedef struct PartitionCmd
 {
 	NodeTag		type;
-	RangeVar   *name;			/* name of partition to attach/detach */
+	RangeVar   *name;			/* name of partition to attach/detach/merge */
 	PartitionBoundSpec *bound;	/* FOR VALUES, if attaching */
+	List	   *partlist;		/* list of partitions, for MERGE
+								 * PARTITION command */
 	bool		concurrent;
 } PartitionCmd;
 
@@ -2473,6 +2475,7 @@ typedef enum AlterTableType
 
index 0000000000..f754849039
@@ -0,0 +1,1068 @@
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, partitions_merge_schema.sales_feb2022) INTO sales_feb_mar_apr2022;
+LINE 1: ...e MERGE PARTITIONS (sales_feb2022, sales_mar2022, partitions...
+-- Test for merged partition properties:
+-- * STATISTICS is empty
+-- * COMMENT is empty
+-- * DEFAULTS are the same as DEFAULTS for partitioned table
+-- * STORAGE is the same as STORAGE for partitioned table
+-- * GENERATED and CONSTRAINTS are the same as GENERATED and CONSTRAINTS for partitioned table
+CREATE TABLE t
+(i int,
+ t text STORAGE EXTENDED COMPRESSION pglz DEFAULT 'default_t',
+ d date GENERATED ALWAYS as ('2022-01-01') STORED) PARTITION BY RANGE (abs(i));
+COMMENT ON COLUMN t.i IS 't1.i';
+CREATE TABLE tp_0_1
+(i int,
+ t text STORAGE MAIN DEFAULT 'default_tp_0_1',
+ d date GENERATED ALWAYS as ('2022-02-02') STORED);
+COMMENT ON COLUMN tp_0_1.i IS 'tp_0_1.i';
+CREATE TABLE tp_1_2
+(i int,
+ t text STORAGE MAIN DEFAULT 'default_tp_1_2',
+ d date GENERATED ALWAYS as ('2022-03-03') STORED);
+COMMENT ON COLUMN tp_1_2.i IS 'tp_1_2.i';
+CREATE STATISTICS t_stat (DEPENDENCIES) on i, b from t;
+CREATE STATISTICS tp_0_1_stat (DEPENDENCIES) on i, b from tp_0_1;
+CREATE STATISTICS tp_1_2_stat (DEPENDENCIES) on i, b from tp_1_2;
+ALTER TABLE t ADD CONSTRAINT t_b_check CHECK (b > 0);
+\d+ tp_0_1
+                                              Table "partitions_merge_schema.tp_0_1"
+--------+---------+-----------+----------+-------------------------------------------------+---------+--------------+-------------
+ i      | integer |           |          |                                                 | plain   |              | tp_0_1.i
+ t      | text    |           |          | 'default_tp_0_1'::text                          | main    |              | 
+ d      | date    |           |          | generated always as ('02-02-2022'::date) stored | plain   |              | 
+Partition of: t FOR VALUES FROM (0) TO (1)
+Partition constraint: ((abs(i) IS NOT NULL) AND (abs(i) >= 0) AND (abs(i) < 1))
+Check constraints:
+    "t_b_check" CHECK (b > 0)
+Statistics objects:
+    "partitions_merge_schema.tp_0_1_stat" (dependencies) ON i, b FROM tp_0_1
+ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_1;
+\d+ tp_0_1
+                                              Table "partitions_merge_schema.tp_0_1"
+--------+---------+-----------+----------+-------------------------------------------------+----------+--------------+-------------
+ i      | integer |           |          |                                                 | plain    |              | 
+ t      | text    |           |          | 'default_t'::text                               | extended |              | 
+ d      | date    |           |          | generated always as ('01-01-2022'::date) stored | plain    |              | 
+Partition constraint: ((abs(i) IS NOT NULL) AND (abs(i) >= 0) AND (abs(i) < 2))
+Check constraints:
+    "t_b_check" CHECK (b > 0)
 
index 0000000000..f8324f01e5
@@ -0,0 +1,708 @@
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, partitions_merge_schema.sales_feb2022) INTO sales_feb_mar_apr2022;
+-- Test for merged partition properties:
+-- * STATISTICS is empty
+-- * COMMENT is empty
+-- * DEFAULTS are the same as DEFAULTS for partitioned table
+-- * STORAGE is the same as STORAGE for partitioned table
+-- * GENERATED and CONSTRAINTS are the same as GENERATED and CONSTRAINTS for partitioned table
+CREATE TABLE t
+(i int,
+ t text STORAGE EXTENDED COMPRESSION pglz DEFAULT 'default_t',
+ d date GENERATED ALWAYS as ('2022-01-01') STORED) PARTITION BY RANGE (abs(i));
+COMMENT ON COLUMN t.i IS 't1.i';
+CREATE TABLE tp_0_1
+(i int,
+ t text STORAGE MAIN DEFAULT 'default_tp_0_1',
+ d date GENERATED ALWAYS as ('2022-02-02') STORED);
+COMMENT ON COLUMN tp_0_1.i IS 'tp_0_1.i';
+CREATE TABLE tp_1_2
+(i int,
+ t text STORAGE MAIN DEFAULT 'default_tp_1_2',
+ d date GENERATED ALWAYS as ('2022-03-03') STORED);
+COMMENT ON COLUMN tp_1_2.i IS 'tp_1_2.i';
+CREATE STATISTICS t_stat (DEPENDENCIES) on i, b from t;
+CREATE STATISTICS tp_0_1_stat (DEPENDENCIES) on i, b from tp_0_1;
+CREATE STATISTICS tp_1_2_stat (DEPENDENCIES) on i, b from tp_1_2;
+ALTER TABLE t ADD CONSTRAINT t_b_check CHECK (b > 0);
+\d+ tp_0_1
+ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_1;
+\d+ tp_0_1

From 1e2c2ee9d580a8726d2d713cfa0e9bca4a1d9ad1 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:58:09 +0300
Subject: [PATCH v42 2/2] Implement ALTER TABLE ... SPLIT PARTITION ... command

 doc/src/sgml/ref/alter_table.sgml             |   95 +-
 src/backend/commands/tablecmds.c              |  416 ++++
 src/test/regress/expected/partition_split.out | 1835 +++++++++++++++++
 src/test/regress/sql/partition_split.sql      | 1142 ++++++++++
 src/tools/pgindent/typedefs.list              |    2 +
 20 files changed, 4578 insertions(+), 19 deletions(-)

index 8201efac86..4f80813412 100644
@@ -1150,6 +1154,74 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-altertable-split-partition">
+    <term><literal>SPLIT PARTITION <replaceable class="parameter">partition_name</replaceable> INTO (PARTITION <replaceable class="parameter">partition_name1</replaceable> { FOR VALUES <replaceable class="parameter">partition_bound_spec</replaceable> | DEFAULT }, PARTITION <replaceable class="parameter">partition_name2</replaceable> { FOR VALUES <replaceable class="parameter">partition_bound_spec</replaceable> | DEFAULT } [, ...])</literal></term>
+
+    <listitem>
+     <para>
+      This form splits a single partition of the target table. Hash-partitioning
+      is not supported. Bounds of new partitions should not overlap with new and
+      existing partitions (except <replaceable class="parameter">partition_name</replaceable>).
+      If the split partition is a <literal>DEFAULT</literal> partition, one of
+      the new partitions must be <literal>DEFAULT</literal>.
+      In case one of the new partitions or one of existing partitions is
+      <literal>DEFAULT</literal>, new partitions <replaceable class="parameter">partition_name1</replaceable>,
+      <replaceable class="parameter">partition_name2</replaceable>, ... can
+      have spaces between partitions bounds.  If the partitioned table does not
+      have a <literal>DEFAULT</literal> partition, the <literal>DEFAULT</literal>
+      partition can be defined as one of the new partitions.
+     </para>
+     <para>
+      In case new partitions do not contain a <literal>DEFAULT</literal>
+      partition and the partitioned table does not have a <literal>DEFAULT</literal>
+      partition, the following must be true: sum bounds of new partitions
+      <replaceable class="parameter">partition_name1</replaceable>,
+      <replaceable class="parameter">partition_name2</replaceable>, ... should
+      be equal to bound of split partition <replaceable class="parameter">partition_name</replaceable>.
+      One of the new partitions <replaceable class="parameter">partition_name1</replaceable>,
+      <replaceable class="parameter">partition_name2</replaceable>, ... can have
+      the same name as split partition <replaceable class="parameter">partition_name</replaceable>
+      (this is suitable in case of splitting a <literal>DEFAULT</literal>
+      partition: we split it, but after splitting we have a partition with the
+      same name).  Only simple, non-partitioned partition can be split.
+     </para>
+     <para>
+      New partitions will have the same owner as the parent partition.
+     </para>
+     <para>
+      It is the user's responsibility to setup <acronym>ACL</acronym> on new
+      partitions.
+     </para>
+     <para>
+      The indexes and identity are created later, after moving the data
+      into the new partitions.
+      Extended statistics aren't copied from the parent table, for consistency with
+      <command>CREATE TABLE PARTITION OF</command>.
+      New partitions will have the same table access method as the parent.
+      If the parent table is persistent then new partitions are created
+      persistent.  If the parent table is temporary then new partitions
+      are also created temporary.  New partitions will also be created in
+      the same tablespace as the parent.
+     </para>
+     <para>
+      If split partition has individual constraints, those constraints will
+      be dropped because command uses partitioned table as a model to create
+      the constraints.
+     </para>
+     <para>
+      If split partition has some objects dependent on it, the command can
+      not be done (<literal>CASCADE</literal> is not used, an error will be returned).
+     </para>
+     <note>
+      <para>
+       This command acquires an <literal>ACCESS EXCLUSIVE</literal> lock.
+       This is a significant limitation, which limits the usage of this
+       command with large partitioned tables under a high load.
+      </para>
+     </note>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-altertable-merge-partitions">
     <term><literal>MERGE PARTITIONS (<replaceable class="parameter">partition_name1</replaceable>, <replaceable class="parameter">partition_name2</replaceable> [, ...]) INTO <replaceable class="parameter">partition_name</replaceable></literal></term>
 
@@ -1245,7 +1317,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1489,7 +1562,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1922,6 +1995,24 @@ ALTER TABLE measurement
 
+  <para>
index d6ce87dd96..3fe46e60e3 100644

 
@@ -22073,6 +22095,254 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 	return cstorage;
 }
 
+/*
+	pc->dstslot = table_slot_create(pc->partRel, NULL);
+	srcslot = table_slot_create(splitRel, NULL);
@@ -22381,6 +22651,152 @@ createPartitionTable(RangeVar *newPartName, Relation modelRel, Oid ownerId)
 	return newRel;
 }
 
+/*

 
+partitions_list:
index 7815e8d245..1b01facffd 100644
 
+/*
@@ -3888,7 +3922,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3896,6 +3930,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3904,7 +3939,11 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
@@ -4339,13 +4378,13 @@ setSchemaName(const char *context_schema, char **stmt_schema_name)
 
 /*
  * transformPartitionCmd
 
@@ -4354,9 +4393,9 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 
@@ -4364,7 +4403,7 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 
+/*
 
+/*
 
 	return buf->data;
 }
+
+/*
 
 	/*
 
index 1ff0e7f6f0..bbd1110a40 100644
@@ -953,15 +953,26 @@ typedef struct PartitionRangeDatum
 
+/*
-	List	   *partlist;		/* list of partitions, for MERGE
@@ -2475,6 +2486,7 @@ typedef enum AlterTableType

 
index 0000000000..ac1be22a32
@@ -0,0 +1,1835 @@
+-- Test for split partition properties:
+-- * STATISTICS is empty
+-- * COMMENT is empty
+-- * DEFAULTS are the same as DEFAULTS for partitioned table
+-- * STORAGE is the same as STORAGE for partitioned table
+-- * GENERATED and CONSTRAINTS are the same as GENERATED and CONSTRAINTS for partitioned table
+CREATE TABLE t
+(i int,
+ t text STORAGE EXTENDED COMPRESSION pglz DEFAULT 'default_t',
+ d date GENERATED ALWAYS as ('2022-01-01') STORED) PARTITION BY RANGE (abs(i));
+COMMENT ON COLUMN t.i IS 't1.i';
+CREATE TABLE tp_x
+(i int,
+ t text STORAGE MAIN DEFAULT 'default_tp_x',
+ d date GENERATED ALWAYS as ('2022-02-02') STORED);
+ALTER TABLE t ATTACH PARTITION tp_x FOR VALUES FROM (0) TO (2);
+COMMENT ON COLUMN tp_x.i IS 'tp_x.i';
+CREATE STATISTICS t_stat (DEPENDENCIES) on i, b from t;
+CREATE STATISTICS tp_x_stat (DEPENDENCIES) on i, b from tp_x;
+ALTER TABLE t ADD CONSTRAINT t_b_check CHECK (b > 0);
+\d+ tp_x
+                                               Table "partition_split_schema.tp_x"
+--------+---------+-----------+----------+-------------------------------------------------+---------+--------------+-------------
+ i      | integer |           |          |                                                 | plain   |              | tp_x.i
+ t      | text    |           |          | 'default_tp_x'::text                            | main    |              | 
+ d      | date    |           |          | generated always as ('02-02-2022'::date) stored | plain   |              | 
+Partition constraint: ((abs(i) IS NOT NULL) AND (abs(i) >= 0) AND (abs(i) < 2))
+Check constraints:
+    "t_b_check" CHECK (b > 0)
+Statistics objects:
+    "partition_split_schema.tp_x_stat" (dependencies) ON i, b FROM tp_x
+ALTER TABLE t SPLIT PARTITION tp_x INTO
+   PARTITION tp_x FOR VALUES FROM (1) TO (2));
+\d+ tp_x
+                                                Table "partition_split_schema.tp_x"
+--------+---------+-----------+----------+-------------------------------------------------+----------+--------------+-------------
+ i      | integer |           |          |                                                 | plain    |              | 
+ t      | text    |           |          | 'default_t'::text                               | extended |              | 
+ d      | date    |           |          | generated always as ('01-01-2022'::date) stored | plain    |              | 
+Partition of: t FOR VALUES FROM (1) TO (2)
+Partition constraint: ((abs(i) IS NOT NULL) AND (abs(i) >= 1) AND (abs(i) < 2))
+Check constraints:
+    "t_b_check" CHECK (b > 0)
 
index 0000000000..9398e6b85d
@@ -0,0 +1,1142 @@
+-- Test for split partition properties:
+-- * STATISTICS is empty
+-- * COMMENT is empty
+-- * DEFAULTS are the same as DEFAULTS for partitioned table
+-- * STORAGE is the same as STORAGE for partitioned table
+-- * GENERATED and CONSTRAINTS are the same as GENERATED and CONSTRAINTS for partitioned table
+CREATE TABLE t
+(i int,
+ t text STORAGE EXTENDED COMPRESSION pglz DEFAULT 'default_t',
+ d date GENERATED ALWAYS as ('2022-01-01') STORED) PARTITION BY RANGE (abs(i));
+COMMENT ON COLUMN t.i IS 't1.i';
+CREATE TABLE tp_x
+(i int,
+ t text STORAGE MAIN DEFAULT 'default_tp_x',
+ d date GENERATED ALWAYS as ('2022-02-02') STORED);
+ALTER TABLE t ATTACH PARTITION tp_x FOR VALUES FROM (0) TO (2);
+COMMENT ON COLUMN tp_x.i IS 'tp_x.i';
+CREATE STATISTICS t_stat (DEPENDENCIES) on i, b from t;
+CREATE STATISTICS tp_x_stat (DEPENDENCIES) on i, b from tp_x;
+ALTER TABLE t ADD CONSTRAINT t_b_check CHECK (b > 0);
+\d+ tp_x
+ALTER TABLE t SPLIT PARTITION tp_x INTO
+   PARTITION tp_x FOR VALUES FROM (1) TO (2));
+\d+ tp_x
index a8346cda63..56ac447288 100644
@@ -2826,6 +2827,7 @@ SpecialJoinInfo
 SpinDelayStatus
 SplitInterval
 SplitLR
+SplitPartitionContext
 SplitPageLayout
 SplitPoint
 SplitTextOutputData
-- 
2.40.1.windows.1

---

From: jian he <[email-masked]>
Date: Tue, 10 Jun 2025 13:50:12 +0800

> (If AM is set for a partitioned table, then use it, otherwise use AM for
I only want to allow HEAP_TABLE_AM_OID to be used
in the merge partition,
I guess that would avoid unintended consequences.

I proposed change was
+if (modelRel->rd_rel->relam != HEAP_TABLE_AM_OID)
+   elog(ERROR, "only heap table method is allowed");
+ relamId = modelRel->rd_rel->relam;

RangeVarGetAndCheckCreationNamespace
was called first on ATExecMergePartitions, then on createPartitionTable.
Maybe we can pass the first  ATExecMergePartitions call result
to createPartitionTable to avoid calling it twice.

CREATE TABLE pp (a int, b int) PARTITION BY LIST(a);
CREATE TABLE pp_p1 PARTITION OF pp FOR VALUES IN (1, 2);
CREATE TABLE pp_p2 PARTITION OF pp FOR VALUES IN (3, 4);
INSERT INTO pp(a, b) SELECT random(min=>1, max=>6),
random(min=>1::int, max=>10) FROM generate_series(0, 4) i;
alter table pp add constraint cc check(a < 0) not valid;

ALTER TABLE pp MERGE PARTITIONS (pp_p1,  pp_p2) INTO pp_p1_2;
src4=# \d+ pp_p1_2
                                         Table "public.pp_p1_2"
 Column |  Type   | Collation | Nullable | Default | Storage |
Compression | Stats target | Description
--------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
 a      | integer |           |          |         | plain   |
    |              |
 b      | integer |           |          |         | plain   |
    |              |
Partition of: pp FOR VALUES IN (1, 2, 3, 4)
Partition constraint: ((a IS NOT NULL) AND (a = ANY (ARRAY[1, 2, 3, 4])))
Check constraints:
    "cc" CHECK (a < 0)
Access method: heap

constraint cc on pp_p1_2 should be NOT VALID.
also if the partitioned table has NOT ENFORCED CHECK constraint, it
will cause segfault.
attached is a possible fix, and related tests.(based on v42).

cosmetic changes:
many of the "forach" can change to "foreach_node".
for example in ATExecMergePartitions.
we can change
``foreach(listptr, cmd->partlist)``
to
``foreach_node(RangeVar, name, cmd->partlist)`

---

From: Junwang Zhao <[email-masked]>
Date: Tue, 10 Jun 2025 23:58:04 +0800

Hi Dmitry,

> But the complex test is probably also interesting.

I had one trivial comment and a question if you don't mind.

Would it be better to use RELATION_IS_OTHER_TEMP in this case?
I noticed that while other parts of tablecmds.c don’t use the macro,
all other files consistently use RELATION_IS_OTHER_TEMP.

+ /*

I have trouble understanding how this is possible, can you kindly
give me some guidance on this logic?

-- 
Regards
Junwang Zhao

---

From: Dmitry Koval <[email-masked]>
Date: Wed, 11 Jun 2025 03:06:27 +0300

Hi!

This email contains comments to three emails
(2 x 06.06.2025 + 1 x 10.06.2025).

1.
 >so I added two, which is necessary IMHO.

I think that a lot of \d+ commands can make the out-files difficult to
read. But I agree, that test for triggers is useful. Test for triggers
added to test of partition properties.

2.
 >see also StoreAttrDefault.

Thanks, this is correct.

 >to simplify the above code.

Should we use ATTRIBUTE_GENERATED_VIRTUAL [1] and
build_generation_expression [2] here? Function expandTableLikeClause
("CREATE TABLE ... LIKE ..." clause) does not use GENERATED VIRTUAL
yet ...

 >this issue i didn't dig deeper.

Probably after commit [3] partition's identity columns shares the
identity space (i.e. underlying sequence) as the corresponding
columns of the partitioned table. So call BuildDescForRelation in
createPartitionTable function should copy pg_attribute.attidentity
for new partition.

 >because later moveMergedTablesRows will use the output of

We call CommandCounterIncrement in createPartitionTable function right
after heap_create_with_catalog (same code in create_toast_table,
make_new_heap, DefineRelation functions). We need an additional
CommandCounterIncrement call in case we use objects created after this
point. But we probably don't use these objects (in function
moveMergedTablesRows too).

3.
 >I guess that would avoid unintended consequences.

Thanks for the clarification. Isn't this limitation too strong?
It is very likely that the user will create an AM based on
HEAP_TABLE_AM_OID, in which case the code should work.

 >was called first on ATExecMergePartitions, then on
 >createPartitionTable. Maybe we can pass the first
 >ATExecMergePartitions call result to createPartitionTable to avoid
 >calling it twice.

Unfortunately, this is not the case for SPLIT PARTITION. I will think
about it, but I am concerned that the createPartitionTable function
will be passed two related arguments - newPartName and namespaceId
(result of RangeVarGetAndCheckCreationNamespace).

Thanks for 
v42-0001-fix-MERGE-PARTITION-with-partitioned-table-not-enforc.no-cfbot! 
I forgot about not valid or not enforced constraints.

 >``foreach_node(RangeVar, name, cmd->partlist)`

Changed.

Links.
------
[1] Virtual generated columns, 
https://github.com/postgres/postgres/commit/83ea6c54025bea67bcd4949a6d58d3fc11c3e21b
[2] Expand virtual generated columns in the planner, 
https://github.com/postgres/postgres/commit/1e4351af329f2949c679a215f63c51d663ecd715
[3] Support identity columns in partitioned tables, 
https://github.com/postgres/postgres/commit/699586315704a8268808e3bdba4cb5924a038c49

P.S. 2Junwang Zhao: sorry, I'll write an answer a little later.

-- 
With best regards,
Dmitry Koval

From cd67f4df61a084d077f6ee95207271dbc3124733 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:57:22 +0300
Subject: [PATCH v43 1/2] Implement ALTER TABLE ... MERGE PARTITIONS ...
 command

Fixes (summary information).
Authors: Alexander Korotkov, Tender Wang, Richard Guo, Dagfinn Ilmari Mannsaker
Authors: Fujii Masao
Reviewed-by: Alexander Korotkov, Robert Haas, Justin Pryzby, Pavel Borisov
Reviewed-by: Masahiko Sawada
Reported-by: Alexander Lakhin, Justin Pryzby, Kyotaro Horiguchi
Reported-by: Daniel Gustafsson, Tom Lane, Noah Misch
---
 doc/src/sgml/ddl.sgml                         |   19 +
 doc/src/sgml/ref/alter_table.sgml             |  105 +-
 src/backend/catalog/heap.c                    |    4 +-
 src/backend/catalog/pg_constraint.c           |    2 +-
 src/backend/commands/tablecmds.c              |  646 ++++++++-
 src/backend/parser/gram.y                     |   22 +-
 src/backend/parser/parse_utilcmd.c            |  144 ++
 src/backend/partitioning/partbounds.c         |  212 ++-
 src/bin/psql/tab-complete.in.c                |   10 +
 src/include/catalog/heap.h                    |    3 +
 src/include/nodes/parsenodes.h                |    5 +-
 src/include/parser/kwlist.h                   |    1 +
 src/include/partitioning/partbounds.h         |    6 +
 .../isolation/expected/partition-merge.out    |  199 +++
 src/test/isolation/isolation_schedule         |    1 +
 src/test/isolation/specs/partition-merge.spec |   54 +
 .../test_ddl_deparse/test_ddl_deparse.c       |    3 +
 src/test/regress/expected/partition_merge.out | 1155 +++++++++++++++++
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/partition_merge.sql      |  766 +++++++++++
 20 files changed, 3330 insertions(+), 29 deletions(-)
 create mode 100644 src/test/isolation/expected/partition-merge.out
 create mode 100644 src/test/isolation/specs/partition-merge.spec
 create mode 100644 src/test/regress/expected/partition_merge.out
 create mode 100644 src/test/regress/sql/partition_merge.sql

 

   <para>
 
+  <para>
 
  <refsect1>
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index fbaed5359a..571b6b0574 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -105,8 +105,6 @@ static void RelationRemoveInheritance(Oid relid);
 static Oid	StoreRelCheck(Relation rel, const char *ccname, Node *expr,
 						  bool is_enforced, bool is_validated, bool is_local,
 						  int16 inhcount, bool is_no_inherit, bool is_internal);
-static void StoreConstraints(Relation rel, List *cooked_constraints,
-							 bool is_internal);
 static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
 										bool allow_merge, bool is_local,
 										bool is_enforced,
@@ -2296,7 +2294,7 @@ StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum,
  * expressions can be added later, by direct calls to StoreAttrDefault
  * and StoreRelCheck (see AddRelationNewConstraints()).
  */
-static void
+void
 StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal)
 {
 	int			numchecks = 0;
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 2d5ac1ea81..1f948876d9 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -875,7 +875,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
 															 false)));
 			constr->is_enforced = true;
 			constr->skip_validation = !conForm->convalidated;
-			constr->initially_valid = true;
+			constr->initially_valid = conForm->convalidated;
 			constr->is_no_inherit = conForm->connoinherit;
 			notnulls = lappend(notnulls, constr);
 		}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ea96947d81..92bed4a2dd 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -740,6 +740,8 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, const char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);

 
 
+/*

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -22035,3 +22072,578 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+			AttrNumber	num;
+			if (attribute->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				this_default = build_generation_expression(modelRel, attribute->attnum);
+				this_default = TupleDescGetDefault(tupleDesc, attribute->attnum);
+						 attribute->attnum, RelationGetRelationName(modelRel));
+			if (found_whole_row && attribute->attgenerated != '\0')
+		bool		ccvalid = constr->check[ccnum].ccvalid;
+		cooked->skip_validation = !ccvalid;
+	StoreConstraints(newRel, cookedConstraints, true);
+		 * We already set pg_attribute.attnotnull in createPartitionTable. No
+		 * need call set_attnotnull again.
+		AddRelationNewConstraints(newRel, NIL, nnconstraints, false, true, true, NULL);
+	foreach_node(RangeVar, name, cmd->partlist)

 

+/*
+ * checkPartition

 				/*
 
 						/*

 

 
index 0000000000..e76cea2a03
@@ -0,0 +1,1155 @@
+-- * TRIGGERS are the same as TRIGGERS for partitioned table
+(i int NOT NULL,
+(i int NOT NULL,
+(i int NOT NULL,
+ALTER TABLE t ADD CONSTRAINT t_b_check1 CHECK (b > 0) NOT ENFORCED;
+ALTER TABLE t ADD CONSTRAINT t_b_check2 CHECK (b > 0) NOT VALID;
+ALTER TABLE t ADD CONSTRAINT t_b_nn NOT NULL b NOT VALID;
+CREATE OR REPLACE FUNCTION trigger_function() RETURNS trigger LANGUAGE 'plpgsql' AS
+$BODY$
+  RETURN new;
+CREATE TRIGGER t_before_insert_row_trigger BEFORE INSERT ON t FOR EACH ROW
+  EXECUTE PROCEDURE trigger_function('t');
+CREATE TRIGGER tp_0_1_before_insert_row_trigger BEFORE INSERT ON tp_0_1 FOR EACH ROW
+  EXECUTE PROCEDURE trigger_function('tp_0_1');
+CREATE TRIGGER tp_1_2_before_insert_row_trigger BEFORE INSERT ON tp_1_2 FOR EACH ROW
+  EXECUTE PROCEDURE trigger_function('tp_1_2');
+ i      | integer |           | not null |                                                 | plain   |              | tp_0_1.i
+ b      | bigint  |           | not null |                                                 | plain   |              | 
+    "t_b_check1" CHECK (b > 0) NOT ENFORCED
+    "t_b_check2" CHECK (b > 0) NOT VALID
+    "tp_0_1_i_not_null" NOT NULL "i" (inherited)
+    "t_b_nn" NOT NULL "b" (inherited) NOT VALID
+Triggers:
+    t_before_insert_row_trigger BEFORE INSERT ON tp_0_1 FOR EACH ROW EXECUTE FUNCTION trigger_function('t'), ON TABLE t
+    tp_0_1_before_insert_row_trigger BEFORE INSERT ON tp_0_1 FOR EACH ROW EXECUTE FUNCTION trigger_function('tp_0_1')
+ b      | bigint  |           | not null |                                                 | plain    |              | 
+    "t_b_check1" CHECK (b > 0) NOT ENFORCED
+    "t_b_check2" CHECK (b > 0) NOT VALID
+    "t_b_nn" NOT NULL "b" (inherited) NOT VALID
+Triggers:
+    t_before_insert_row_trigger BEFORE INSERT ON tp_0_1 FOR EACH ROW EXECUTE FUNCTION trigger_function('t'), ON TABLE t
+DROP FUNCTION trigger_function();
+-- Test MERGE PARTITIONS with not valid foreign key constraint
+CREATE TABLE t (i INT PRIMARY KEY) PARTITION BY RANGE (i);
+INSERT INTO t VALUES (0), (1);
+CREATE TABLE t_fk (i INT);
+INSERT INTO t_fk VALUES (1), (2);
+ALTER TABLE t_fk ADD CONSTRAINT t_fk_i_fkey FOREIGN KEY (i) REFERENCES t NOT VALID;
+-- Should be NOT VALID FOREIGN KEY
+\d tp_0_2
+      Table "partitions_merge_schema.tp_0_2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ i      | integer |           | not null | 
+    "tp_0_2_pkey" PRIMARY KEY, btree (i)
+Referenced by:
+    TABLE "t_fk" CONSTRAINT "t_fk_i_fkey" FOREIGN KEY (i) REFERENCES t(i) NOT VALID
+-- ERROR:  insert or update on table "t_fk" violates foreign key constraint "t_fk_i_fkey"
+ALTER TABLE t_fk VALIDATE CONSTRAINT t_fk_i_fkey;
+ERROR:  insert or update on table "t_fk" violates foreign key constraint "t_fk_i_fkey"
+DETAIL:  Key (i)=(2) is not present in table "t".
+DROP TABLE t_fk;
+-- Test MERGE PARTITIONS with not enforced foreign key constraint
+CREATE TABLE t (i INT PRIMARY KEY) PARTITION BY RANGE (i);
+INSERT INTO t VALUES (0), (1);
+CREATE TABLE t_fk (i INT);
+INSERT INTO t_fk VALUES (1), (2);
+ALTER TABLE t_fk ADD CONSTRAINT t_fk_i_fkey FOREIGN KEY (i) REFERENCES t NOT ENFORCED;
+-- Should be NOT ENFORCED FOREIGN KEY
+\d tp_0_2
+      Table "partitions_merge_schema.tp_0_2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ i      | integer |           | not null | 
+    "tp_0_2_pkey" PRIMARY KEY, btree (i)
+Referenced by:
+    TABLE "t_fk" CONSTRAINT "t_fk_i_fkey" FOREIGN KEY (i) REFERENCES t(i) NOT ENFORCED
+-- ERROR:  insert or update on table "t_fk" violates foreign key constraint "t_fk_i_fkey"
+ALTER TABLE t_fk ALTER CONSTRAINT t_fk_i_fkey ENFORCED;
+ERROR:  insert or update on table "t_fk" violates foreign key constraint "t_fk_i_fkey"
+DETAIL:  Key (i)=(2) is not present in table "t".
+DROP TABLE t_fk;
 
index 0000000000..d72af062ee
@@ -0,0 +1,766 @@
+-- * TRIGGERS are the same as TRIGGERS for partitioned table
+(i int NOT NULL,
+(i int NOT NULL,
+(i int NOT NULL,
+ALTER TABLE t ADD CONSTRAINT t_b_check1 CHECK (b > 0) NOT ENFORCED;
+ALTER TABLE t ADD CONSTRAINT t_b_check2 CHECK (b > 0) NOT VALID;
+ALTER TABLE t ADD CONSTRAINT t_b_nn NOT NULL b NOT VALID;
+CREATE OR REPLACE FUNCTION trigger_function() RETURNS trigger LANGUAGE 'plpgsql' AS
+$BODY$
+  RETURN new;
+CREATE TRIGGER t_before_insert_row_trigger BEFORE INSERT ON t FOR EACH ROW
+  EXECUTE PROCEDURE trigger_function('t');
+CREATE TRIGGER tp_0_1_before_insert_row_trigger BEFORE INSERT ON tp_0_1 FOR EACH ROW
+  EXECUTE PROCEDURE trigger_function('tp_0_1');
+CREATE TRIGGER tp_1_2_before_insert_row_trigger BEFORE INSERT ON tp_1_2 FOR EACH ROW
+  EXECUTE PROCEDURE trigger_function('tp_1_2');
+DROP FUNCTION trigger_function();
+-- Test MERGE PARTITIONS with not valid foreign key constraint
+CREATE TABLE t (i INT PRIMARY KEY) PARTITION BY RANGE (i);
+INSERT INTO t VALUES (0), (1);
+CREATE TABLE t_fk (i INT);
+INSERT INTO t_fk VALUES (1), (2);
+ALTER TABLE t_fk ADD CONSTRAINT t_fk_i_fkey FOREIGN KEY (i) REFERENCES t NOT VALID;
+-- Should be NOT VALID FOREIGN KEY
+\d tp_0_2
+-- ERROR:  insert or update on table "t_fk" violates foreign key constraint "t_fk_i_fkey"
+ALTER TABLE t_fk VALIDATE CONSTRAINT t_fk_i_fkey;
+DROP TABLE t_fk;
+-- Test MERGE PARTITIONS with not enforced foreign key constraint
+CREATE TABLE t (i INT PRIMARY KEY) PARTITION BY RANGE (i);
+INSERT INTO t VALUES (0), (1);
+CREATE TABLE t_fk (i INT);
+INSERT INTO t_fk VALUES (1), (2);
+ALTER TABLE t_fk ADD CONSTRAINT t_fk_i_fkey FOREIGN KEY (i) REFERENCES t NOT ENFORCED;
+-- Should be NOT ENFORCED FOREIGN KEY
+\d tp_0_2
+-- ERROR:  insert or update on table "t_fk" violates foreign key constraint "t_fk_i_fkey"
+ALTER TABLE t_fk ALTER CONSTRAINT t_fk_i_fkey ENFORCED;
+DROP TABLE t_fk;

From 680b96df014a7323ba2bf22b3c0875e300fedd68 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:58:09 +0300
Subject: [PATCH v43 2/2] Implement ALTER TABLE ... SPLIT PARTITION ... command

 src/backend/commands/tablecmds.c              |  414 ++++
 src/backend/parser/parse_utilcmd.c            |   56 +-
 src/backend/partitioning/partbounds.c         |  677 ++++++
 src/test/regress/expected/partition_split.out | 1866 +++++++++++++++++
 src/test/regress/sql/partition_split.sql      | 1160 ++++++++++
 20 files changed, 4610 insertions(+), 19 deletions(-)

 

+  <para>
index 92bed4a2dd..922abf7284 100644

 
 
+/*
+	foreach_ptr(SplitPartitionContext, spc, partContexts)
+		deleteSplitPartitionContext(spc, ti_options);
@@ -22383,6 +22653,150 @@ createPartitionTable(RangeVar *newPartName, Relation modelRel, Oid ownerId)
 	return newRel;
 }
 
+/*
+	foreach_node(SinglePartitionSpec, sps, cmd->partlist)
+	foreach_node(SinglePartitionSpec, sps, cmd->partlist)

 
+partitions_list:
index 7815e8d245..7e1f64cf50 100644
@@ -3529,6 +3529,37 @@ checkPartition(Relation rel, Oid partRelOid)
 
+/*
+	foreach_node(SinglePartitionSpec, sps, partcmd->partlist)
@@ -3888,7 +3919,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3896,6 +3927,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3904,7 +3936,11 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
@@ -4339,13 +4375,13 @@ setSchemaName(const char *context_schema, char **stmt_schema_name)
 
 /*
  * transformPartitionCmd
 
@@ -4354,9 +4390,9 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 
@@ -4364,7 +4400,7 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
index cb719f1ff5..026c2b80b3 100644
 
+/*
@@ -5077,6 +5136,624 @@ get_partition_bound_spec(Oid partOid, RangeVar *name)
 	return boundspec;
 }
 
+/*
+	foreach_node(Const, val, spec->listdatums)
+	for (int i = 0; i < nparts; i++)
+		foreach_node(Const, val, sps->bound->listdatums)
+				foreach_node(SinglePartitionSpec, sps, partlist)
 
 	return buf->data;
 }
+
+/*
 
 	/*

+/*

 
index 0000000000..df1c29b419
@@ -0,0 +1,1866 @@
+-- * TRIGGERS are the same as TRIGGERS for partitioned table
+(i int NOT NULL,
+(i int NOT NULL,
+ALTER TABLE t ADD CONSTRAINT t_b_check1 CHECK (b > 0) NOT ENFORCED;
+ALTER TABLE t ADD CONSTRAINT t_b_check2 CHECK (b > 0) NOT VALID;
+ALTER TABLE t ADD CONSTRAINT t_b_nn NOT NULL b NOT VALID;
+CREATE OR REPLACE FUNCTION trigger_function() RETURNS trigger LANGUAGE 'plpgsql' AS
+$BODY$
+  RETURN new;
+CREATE TRIGGER t_before_insert_row_trigger BEFORE INSERT ON t FOR EACH ROW
+  EXECUTE PROCEDURE trigger_function('t');
+CREATE TRIGGER tp_x_before_insert_row_trigger BEFORE INSERT ON tp_x FOR EACH ROW
+  EXECUTE PROCEDURE trigger_function('tp_x');
+ i      | integer |           | not null |                                                 | plain   |              | tp_x.i
+ b      | bigint  |           | not null |                                                 | plain   |              | 
+    "t_b_check1" CHECK (b > 0) NOT ENFORCED
+    "t_b_check2" CHECK (b > 0) NOT VALID
+    "tp_x_i_not_null" NOT NULL "i" (inherited)
+    "t_b_nn" NOT NULL "b" (inherited) NOT VALID
+Triggers:
+    t_before_insert_row_trigger BEFORE INSERT ON tp_x FOR EACH ROW EXECUTE FUNCTION trigger_function('t'), ON TABLE t
+    tp_x_before_insert_row_trigger BEFORE INSERT ON tp_x FOR EACH ROW EXECUTE FUNCTION trigger_function('tp_x')
+ b      | bigint  |           | not null |                                                 | plain    |              | 
+    "t_b_check1" CHECK (b > 0) NOT ENFORCED
+    "t_b_check2" CHECK (b > 0) NOT VALID
+    "t_b_nn" NOT NULL "b" (inherited) NOT VALID
+Triggers:
+    t_before_insert_row_trigger BEFORE INSERT ON tp_x FOR EACH ROW EXECUTE FUNCTION trigger_function('t'), ON TABLE t
+DROP FUNCTION trigger_function();
 
index 0000000000..7664937692
@@ -0,0 +1,1160 @@
+-- * TRIGGERS are the same as TRIGGERS for partitioned table
+(i int NOT NULL,
+(i int NOT NULL,
+ALTER TABLE t ADD CONSTRAINT t_b_check1 CHECK (b > 0) NOT ENFORCED;
+ALTER TABLE t ADD CONSTRAINT t_b_check2 CHECK (b > 0) NOT VALID;
+ALTER TABLE t ADD CONSTRAINT t_b_nn NOT NULL b NOT VALID;
+CREATE OR REPLACE FUNCTION trigger_function() RETURNS trigger LANGUAGE 'plpgsql' AS
+$BODY$
+  RETURN new;
+CREATE TRIGGER t_before_insert_row_trigger BEFORE INSERT ON t FOR EACH ROW
+  EXECUTE PROCEDURE trigger_function('t');
+CREATE TRIGGER tp_x_before_insert_row_trigger BEFORE INSERT ON tp_x FOR EACH ROW
+  EXECUTE PROCEDURE trigger_function('tp_x');
+DROP FUNCTION trigger_function();

---

From: jian he <[email-masked]>
Date: Wed, 11 Jun 2025 12:45:17 +0800

hi.

we generally no need to worry about the partitioned table check constraint,
generated column does not apply to newly merged partitions.
since partitioned table constraints apply to each individual partition,
including newly created partitions.

However, there are corner cases:
constraints include tableoid column reference.
say pk_1, pk_2 is the partition of pk.
alter table pk add constraint cc check(tableoid <> 18259);

Here, Constraint cc will check whether the tableoid of pk_1 or pk_2 meets the
specified criteria. Similarly, if partitions are merged, the newly merged
partition must also be evaluated against the constraint cc to ensure
it is satisfied.

----------------
The attached patch ensures that the newly merged partition is evaluated against
all of its check constraints and that all stored generated columns are
recomputed, i guess this would be more safe.

Alternatively, we can use pull_varattnos to check whether a constraint or
generated expression contains a reference to the tableoid column.

---

From: jian he <[email-masked]>
Date: Wed, 11 Jun 2025 15:28:36 +0800

> createPartitionTable function should copy pg_attribute.attidentity
but BuildDescForRelation is based on getAttributesList,
in getAttributesList, assign pg_attribute.attidentity to def->identity
should be safe, IMHO.

+     <para>
+      If merged partitions have different owners, an error will be generated.
+      The owner of the merged partitions will be the owner of the new
partition.
+     </para>
+     <para>
+      It is the user's responsibility to setup <acronym>ACL</acronym> on the
+      new partition.
+     </para>
since they <para> are related, these two can be one <para>?

+     <para>

I feel like it's not fully accurate, the following is what I can come up with:
+     <para>
+ When partitions are merged, any individual objects belonging to those
+ partitions, such as constraints or statistics will be dropped. This occurs
+ because ALTER TABLE MERGE PARTITIONS uses the partitioned table itself as the
+ template to define these objects.
+     </para>

> moveMergedTablesRows too).
As mentioned in the previous thread [1], moveMergedTablesRows need
latest relcache entry for newPartRel. so I guess, put one
CommandCounterIncrement at the end of createPartitionTable
should be fine, which I already did in [1].
[1]: https://postgr.es/m/[email-masked]

> HEAP_TABLE_AM_OID, in which case the code should work.
ok.

if you looking at ATExecDetachPartition, we have:
    /*
     * Detaching the partition might involve TOAST table access, so ensure we
     * have a valid snapshot.
     */
    PushActiveSnapshot(GetTransactionSnapshot());
    /* Do the final part of detaching */
    DetachPartitionFinalize(rel, partRel, concurrent, defaultPartOid);
    PopActiveSnapshot();

do we need do the same to the following DetachPartitionFinalize:
    foreach_ptr(RelationData, mergingPartition, mergingPartitionsList)
    {
        /* Remove the pg_inherits row first. */
        RemoveInheritance(mergingPartition, rel, false);
        /* Do the final part of detaching. */
        DetachPartitionFinalize(rel, mergingPartition, false, defaultPartOid);
    }

---

From: Dmitry Koval <[email-masked]>
Date: Wed, 11 Jun 2025 16:10:00 +0300

Hi, Junwang Zhao!

Thank you for note.

1.
 >all other files consistently use RELATION_IS_OTHER_TEMP.

Agreed, RELATION_IS_OTHER_TEMP is better. Changed.
The fix will be in the next patch.

2.
 >+/*
 >+* We intended to create the partition with the same persistence as the
 >+* parent table, but we still need to recheck because that might be
 >+* affected by the search_path.  If the parent is permanent, so must be
 >+* all of its partitions.
 >+*/

 >give me some guidance on this logic?

Perhaps this is best explained with an example.
(see src/test/regress/sql/partition_merge.sql).

(a) Create permanent table "t":

SET search_path = partitions_merge_schema, pg_temp, public;
CREATE TABLE t (i int) 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);

(b) Attempt to merge persistent partitions tp_0_1, tp_1_2 into
temporary partition tp_0_2:

SET search_path = pg_temp, partitions_merge_schema, public;
-- Can't merge persistent partitions into a temporary partition
ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2;

(c) "ALTER TABLE ... MERGE PARTITIONS ..." will return an error:

ERROR:  cannot create a temporary relation as partition of permanent 
relation "t"

-- 
With best regards,
Dmitry Koval

---

From: jian he <[email-masked]>
Date: Wed, 11 Jun 2025 21:22:22 +0800

hi.

In here, should performDeletion last flags have PERFORM_DELETION_INTERNAL?

also this is not ideal, imagine you first did all the main work in
moveMergedTablesRows,
then suddenly error out, saying:

ERROR:  cannot drop table public.pk_1 because other objects depend on it
DETAIL:  view public.v1 depends on table public.pk_1
HINT:  Use DROP ... CASCADE to drop the dependent objects too.
(this errhint, seems not ideal in this context)

We can perform a preliminary check to determine whether dropping a partition is
allowed, and raise an error if it's not. To do it, I invented a new
function, performDeletionCheck to verify whether an object can be
safely dropped.

please check attached, it was based on v43.

---

From: jian he <[email-masked]>
Date: Thu, 12 Jun 2025 15:31:04 +0800

hi.

+/*
check_partitions_for_split does not exist in v43-0001.

+ /*
the third argument, is_internal should set to true?

a partitioned table can not have NO INHERIT check constraint,
you may see StoreRelCheck.
we can add an Assert: Assert(!ccnoinherit);

+ /* Reproduce not-null constraints. */
+ if (constr->has_not_null)
+ {
+ List   *nnconstraints;
+
+ nnconstraints = RelationGetNotNullConstraints(RelationGetRelid(modelRel),
+  false, true);
as mentioned in above, partitioned table cannot have NO INHERIT constraint,
maybe we should set RelationGetNotNullConstraints last argument to false

/*
 * calculate_partition_bound_for_merge
 *
 * Calculates the bound of merged partition "spec" by using the bounds of
 * partitions to be merged.
 *
 * parent:            partitioned table
 * partNames:        names of partitions to be merged
 * partOids:        Oids of partitions to be merged
 * spec (out):        bounds specification of the merged partition
 * pstate:            pointer to ParseState struct for determine error position
 */
void
calculate_partition_bound_for_merge(Relation parent,
                                    List *partNames,
                                    List *partOids,
                                    PartitionBoundSpec *spec,
                                    ParseState *pstate)

if we are within calculate_partition_bound_for_merge,
then we at least hold AccessShareLock for all the partOids
(see transformPartitionCmdForMerge)
partNames is a list of RangeVar one to one corresponding to partOids,
then we really do not need partNames at all for error messages handling, we can
just use get_rel_name.
so we don't need to pass partNames to calculate_partition_bound_for_merge
The attached patch is a rewrite for
calculate_partition_bound_for_merge and callees.
please let me know whether this improves code readability

in RelationBuildPartitionDesc
we have the following code pattern:
    foreach(cell, inhoids)
    {
        Oid            inhrelid = lfirst_oid(cell);
        HeapTuple    tuple;
        PartitionBoundSpec *boundspec = NULL;
        /* Try fetching the tuple from the catcache, for speed. */
        tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(inhrelid));
        if (HeapTupleIsValid(tuple))
        {
            datum = SysCacheGetAttr(RELOID, tuple,
                                    Anum_pg_class_relpartbound,
                                    &isnull);
            if (!isnull)
                boundspec = stringToNode(TextDatumGetCString(datum));
            ReleaseSysCache(tuple);
        }
        if (boundspec == NULL)
        {
            pg_class = table_open(RelationRelationId, AccessShareLock);
            ScanKeyInit(&key[0],
                        Anum_pg_class_oid,
                        BTEqualStrategyNumber, F_OIDEQ,
                        ObjectIdGetDatum(inhrelid));
            scan = systable_beginscan(pg_class, ClassOidIndexId, true,
                                      NULL, 1, key);
....
}
I wonder if we should do the same in get_partition_bound_spec?
you may also see comments in RelationBuildPartitionDesc, partdesc.c line 203.

---

From: jian he <[email-masked]>
Date: Thu, 12 Jun 2025 19:03:13 +0800

hi.
one more minor issue.

v43-0001 doesn't have the SPLIT PARTITION feature.
maybe we need to remove the argument (bool defaultPart)
from check_two_partitions_bounds_range, aslo remove the comments.
then we can add it on 0002 SPLIT PARTITION patch.

---

From: Dmitry Koval <[email-masked]>
Date: Thu, 12 Jun 2025 23:36:25 +0300

Hi, Jian He!

Thanks for the notes and patches (again).
I read a part of emails, I hope to read the rest emails tomorrow.

1.
 >The attached patch ensures that the newly merged partition is
 >evaluated against all of its check constraints and that all stored
 >generated columns are recomputed, i guess this would be more safe.
 >v43-0001-MERGE-PARTITIONS-constraint-revalidation.no-cfbot

I modified the patch to apply it to the SPLIT PARTITION command too.

2.
 >in getAttributesList, assign pg_attribute.attidentity to def->identity

You are right. Corrected.

3.
+<para>
+If merged partitions have different owners, an error will be generated

Changed.

4.
 >+<para>
 >+ When partitions are merged, any individual objects belonging to

Changed.

5.
 > /*
 >  * Detaching the partition might involve TOAST table access, so ensure
 >  * we have a valid snapshot.
 >  */

Thanks. This needs to be done, especially after the recent commit [1].
Fixed.

Links.
------
[1] Ensure we have a snapshot when updating various system catalogs, 
https://github.com/postgres/postgres/commit/706054b11b959c865c0c7935c34d92370d7168d4

-- 
With best regards,
Dmitry Koval

From 56af1c9d9ebb5f5bf729ab0969d0e3fce9c71f7d Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:57:22 +0300
Subject: [PATCH v44 1/2] Implement ALTER TABLE ... MERGE PARTITIONS ...
 command

Authors: Fujii Masao, Jian He
 doc/src/sgml/ref/alter_table.sgml             |  104 +-
 src/backend/commands/tablecmds.c              |  848 +++++++++++-
 src/test/regress/expected/partition_merge.out | 1167 +++++++++++++++++
 src/test/regress/sql/partition_merge.sql      |  770 +++++++++++
 20 files changed, 3547 insertions(+), 29 deletions(-)

index 96936bcd3a..62c2bb54ea 100644
@@ -4440,6 +4440,25 @@ ALTER TABLE measurement_y2006m02 ADD UNIQUE (city_id, logdate);
index d63f3a621a..855d2ca44d 100644

@@ -1147,14 +1150,101 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+      because <command>ALTER TABLE MERGE PARTITIONS</command> uses the partitioned table itself as the
 
   <para>
@@ -1397,7 +1487,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1830,6 +1921,13 @@ ALTER TABLE measurement
 
+  <para>
 
  <refsect1>
index ea96947d81..190ce952ad 100644

 
 
+/*

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -22035,3 +22072,780 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+
+/*
+ * buildExpressionExecutionStates: build the needed expression execution states
+ * for new partition (newPartRel) checks and initialize expressions for
+ * generated columns. All expressions should be created in "tab"
+ * (AlteredTableInfo structure).
+ */
+static void
+buildExpressionExecutionStates(AlteredTableInfo *tab, Relation newPartRel, EState *estate)
+{
+	/* Build the needed expression execution states. */
+	foreach_ptr(NewConstraint, con, tab->constraints)
+	{
+		switch (con->contype)
+		{
+			case CONSTR_CHECK:
+				con->qualstate = ExecPrepareExpr((Expr *) expand_generated_columns_in_expr(con->qual, newPartRel, 1), estate);
+				break;
+			case CONSTR_FOREIGN:
+				/* Nothing to do here. */
+				break;
+			case CONSTR_NOTNULL:
+				/* Nothing to do here. */
+				break;
+			default:
+				elog(ERROR, "unrecognized constraint type: %d",
+					 (int) con->contype);
+		}
+	}
+
+	foreach_ptr(NewColumnValue, ex, tab->newvals)
+	{
+		/* Expression already planned. */
+		ex->exprstate = ExecInitExpr((Expr *) ex->expr, NULL);
+	}
+}
+
+/*
+ * evaluateGeneratedExpressionsAndCheckConstraints: evaluate any generated
+ * expressions for "tab" (AlteredTableInfo structure) whose inputs come from
+ * the new tuple (insertslot) of new partition (newPartRel).
+ */
+static void
+evaluateGeneratedExpressionsAndCheckConstraints(AlteredTableInfo *tab,
+												Relation newPartRel,
+												TupleTableSlot *insertslot,
+												ExprContext *econtext)
+{
+	econtext->ecxt_scantuple = insertslot;
+
+	foreach_ptr(NewColumnValue, ex, tab->newvals)
+	{
+		if (!ex->is_generated)
+			continue;
+
+		insertslot->tts_values[ex->attnum - 1]
+			= ExecEvalExpr(ex->exprstate,
+							econtext,
+							&insertslot->tts_isnull[ex->attnum - 1]);
+	}
+
+	foreach_ptr(NewConstraint, con, tab->constraints)
+	{
+		switch (con->contype)
+		{
+			case CONSTR_CHECK:
+				if (!ExecCheck(con->qualstate, econtext))
+					ereport(ERROR,
+							errcode(ERRCODE_CHECK_VIOLATION),
+							errmsg("check constraint \"%s\" of relation \"%s\" is violated by some row",
+								   con->name, RelationGetRelationName(newPartRel)),
+							errtableconstraint(newPartRel, con->name));
+				break;
+			case CONSTR_NOTNULL:
+			case CONSTR_FOREIGN:
+				/* Nothing to do here */
+				break;
+			default:
+				elog(ERROR, "unrecognized constraint type: %d",
+					 (int) con->contype);
+		}
+	}
+}
+
+/*
+ * getAttributesList: return list of columns (ColumnDef) like model table
+ * (modelRel)
+ */
+static List *
+getAttributesList(Relation modelRel)
+{
+	AttrNumber	parent_attno;
+	TupleDesc	modelDesc;
+	List	   *colList = NIL;
+
+	modelDesc = RelationGetDescr(modelRel);
+
+	for (parent_attno = 1; parent_attno <= modelDesc->natts;
+		 parent_attno++)
+	{
+		Form_pg_attribute attribute = TupleDescAttr(modelDesc,
+													parent_attno - 1);
+		ColumnDef  *def;
+
+		/* Ignore dropped columns in the parent. */
+		if (attribute->attisdropped)
+			continue;
+
+		def = makeColumnDef(NameStr(attribute->attname), attribute->atttypid,
+							attribute->atttypmod, attribute->attcollation);
+
+		def->is_not_null = attribute->attnotnull;
+
+		/* Copy identity for new partition. */
+		def->identity = attribute->attidentity;
+
+		/* Add to column list */
+		colList = lappend(colList, def);
+
+		/*
+		 * Although we don't transfer the column's default/generation
+		 * expression now, we need to mark it GENERATED if appropriate.
+		 */
+		if (attribute->atthasdef && attribute->attgenerated)
+			def->generated = attribute->attgenerated;
+
+		def->storage = attribute->attstorage;
+
+		/* Likewise, copy compression if requested */
+		if (CompressionMethodIsValid(attribute->attcompression))
+			def->compression =
+				pstrdup(GetCompressionMethodName(attribute->attcompression));
+		else
+			def->compression = NULL;
+	}
+
+	return colList;
+}
+
+
+/*
+ * createTableConstraints: create constraints, default values and generated
+ * values (prototype is function expandTableLikeClause).
+ * tab is pending-work queue for newRel, we may need it in moveMergedTablesRows.
+ */
+static void
+createTableConstraints(List **wqueue, AlteredTableInfo *tab,
+					   Relation modelRel, Relation newRel)
+{
+	TupleDesc	tupleDesc;
+	TupleConstr *constr;
+	AttrMap    *attmap;
+	AttrNumber	parent_attno;
+	int			ccnum;
+	ListCell   *lcon;
+	List	   *cookedConstraints = NIL;
+
+	tupleDesc = RelationGetDescr(modelRel);
+	constr = tupleDesc->constr;
+
+	if (!constr)
+		return;
+
+	/*
+	 * Construct a map from the LIKE relation's attnos to the child rel's.
+	 * This re-checks type match etc, although it shouldn't be possible to
+	 * have a failure since both tables are locked.
+	 */
+	attmap = build_attrmap_by_name(RelationGetDescr(newRel),
+								   tupleDesc,
+								   false);
+
+	/* Cycle for default values. */
+	for (parent_attno = 1; parent_attno <= tupleDesc->natts; parent_attno++)
+	{
+		Form_pg_attribute attribute = TupleDescAttr(tupleDesc,
+													parent_attno - 1);
+
+		/* Ignore dropped columns in the parent. */
+		if (attribute->attisdropped)
+			continue;
+
+		/* Copy default, if present and it should be copied. */
+		if (attribute->atthasdef)
+		{
+			Node	   *this_default = NULL;
+			bool		found_whole_row;
+			AttrNumber	num;
+			Node	   *def;
+			NewColumnValue *newval;
+
+			if (attribute->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				this_default = build_generation_expression(modelRel, attribute->attnum);
+			else
+			{
+				this_default = TupleDescGetDefault(tupleDesc, attribute->attnum);
+				if (this_default == NULL)
+					elog(ERROR, "default expression not found for attribute %d of relation \"%s\"",
+						 attribute->attnum, RelationGetRelationName(modelRel));
+			}
+
+			num = attmap->attnums[parent_attno - 1];
+			def = map_variable_attnos(this_default, 1, 0, attmap, InvalidOid, &found_whole_row);
+
+			/*
+			 * Prevent this for the same reason as for constraints below. Note
+			 * that defaults cannot contain any vars, so it's OK that the
+			 * error message refers to generated columns.
+			 */
+			if (found_whole_row && attribute->attgenerated != '\0')
+				ereport(ERROR,
+						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						errmsg("cannot convert whole-row table reference"),
+						errdetail("Generation expression for column \"%s\" contains a whole-row reference to table \"%s\".",
+								 NameStr(attribute->attname),
+								 RelationGetRelationName(modelRel)));
+
+			/* Add a pre-cooked default expression. */
+			StoreAttrDefault(newRel, num, def, true);
+
+			/*
+			 * Stored generated column expressions in modelRel might reference
+			 * tableoid.  newRel, modelRel tableoid clear is not the same. If
+			 * so, these stored generated columns require recomputation for
+			 * newRel within moveMergedTablesRows.
+			*/
+			if (attribute->attgenerated == ATTRIBUTE_GENERATED_STORED)
+			{
+				newval = (NewColumnValue *) palloc0(sizeof(NewColumnValue));
+				newval->attnum = num;
+				newval->expr = expression_planner((Expr *) def);
+				newval->is_generated = (attribute->attgenerated != '\0');
+				tab->newvals = lappend(tab->newvals, newval);
+			}
+		}
+	}
+
+	/* Cycle for CHECK constraints. */
+	for (ccnum = 0; ccnum < constr->num_check; ccnum++)
+	{
+		char	   *ccname = constr->check[ccnum].ccname;
+		char	   *ccbin = constr->check[ccnum].ccbin;
+		bool		ccenforced = constr->check[ccnum].ccenforced;
+		bool		ccnoinherit = constr->check[ccnum].ccnoinherit;
+		bool		ccvalid = constr->check[ccnum].ccvalid;
+		Node	   *ccbin_node;
+		bool		found_whole_row;
+		CookedConstraint *cooked;
+
+		ccbin_node = map_variable_attnos(stringToNode(ccbin),
+										 1, 0,
+										 attmap,
+										 InvalidOid, &found_whole_row);
+
+		/*
+		 * We reject whole-row variables because the whole point of LIKE is
+		 * that the new table's rowtype might later diverge from the parent's.
+		 * So, while translation might be possible right now, it wouldn't be
+		 * possible to guarantee it would work in future.
+		 */
+		if (found_whole_row)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot convert whole-row table reference"),
+					errdetail("Constraint \"%s\" contains a whole-row reference to table \"%s\".",
+							   ccname,
+							   RelationGetRelationName(modelRel)));
+
+		cooked = (CookedConstraint *) palloc(sizeof(CookedConstraint));
+		cooked->contype = CONSTR_CHECK;
+		cooked->conoid = InvalidOid;
+		cooked->name = ccname;
+		cooked->attnum = 0;
+		cooked->expr = ccbin_node;
+		cooked->is_enforced = ccenforced;
+		cooked->skip_validation = !ccvalid;
+		cooked->is_local = true;
+		cooked->inhcount = 0;
+		cooked->is_no_inherit = ccnoinherit;
+		cookedConstraints = lappend(cookedConstraints, cooked);
+	}
+
+	/* Store CHECK constraints. */
+	StoreConstraints(newRel, cookedConstraints, true);
+
+	/*
+	 * modelRel check constraint expresssion may reference tableoid, so later in
+	 * moveMergedTablesRows, we need evulate the check constraint again for the
+	 * newRel. We can check weather check constraint contain tableoid reference
+	 * or not via pull_varattnos. But unconditionaly revaulate check cosntraint
+	 * seems more safe.
+	*/
+	foreach(lcon, cookedConstraints)
+	{
+		CookedConstraint *ccon = (CookedConstraint *) lfirst(lcon);
+
+		if (!ccon->skip_validation && ccon->contype == CONSTR_CHECK)
+		{
+			NewConstraint *newcon;
+			newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
+			newcon->name = ccon->name;
+			newcon->contype = ccon->contype;
+			newcon->qual = ccon->expr;
+
+			tab->constraints = lappend(tab->constraints, newcon);
+		}
+	}
+
+	/* Don't need the cookedConstraints any more. */
+	list_free_deep(cookedConstraints);
+
+	/* Reproduce not-null constraints. */
+	if (constr->has_not_null)
+	{
+		List	   *nnconstraints;
+
+		nnconstraints = RelationGetNotNullConstraints(RelationGetRelid(modelRel),
+													  false, true);
+
+		Assert(list_length(nnconstraints) > 0);
+
+		/*
+		 * We already set pg_attribute.attnotnull in createPartitionTable. No
+		 * need call set_attnotnull again.
+		*/
+		AddRelationNewConstraints(newRel, NIL, nnconstraints, false, true, true, NULL);
+	}
+}
+
+
+/*
+ * createPartitionTable: create table for a new partition with given name
+ * (newPartName) like table (modelRel, partitioned table). ownerId is
+ * determined by the partition on which the operation is performed, so it
+ * is passed separately.
+ *
+ * Also, this function sets the new partition access method same as parent
+ * table access methods (similarly to CREATE TABLE ... PARTITION OF).  It
+ * checks that parent and child tables have compatible persistence.
+ *
+ * Function returns the created relation (locked in AccessExclusiveLock mode).
+ */
+static Relation
+createPartitionTable(List **wqueue, RangeVar *newPartName,
+					 Relation modelRel, Oid ownerId)
+{
+	Relation	newRel;
+	Oid			newRelId;
+	Oid			existingRelid;
+	TupleDesc	descriptor;
+	List	   *colList = NIL;
+	Oid			relamId;
+	Oid			namespaceId;
+	AlteredTableInfo *new_partrel_tab;
+
+	/* If existing rel is temp, it must belong to this session */
+	if (RELATION_IS_OTHER_TEMP(modelRel))
+		ereport(ERROR,
+				errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				errmsg("cannot create as partition of temporary relation of another session"));
+
+	/* Look up inheritance ancestors and generate relation schema. */
+	colList = getAttributesList(modelRel);
+
+	/* Create a tuple descriptor from the relation schema. */
+	descriptor = BuildDescForRelation(colList);
+
+	/* Look up the access method for new relation. */
+	relamId = (modelRel->rd_rel->relam != InvalidOid) ? modelRel->rd_rel->relam : HEAP_TABLE_AM_OID;
+
+	/* Look up the namespace in which we are supposed to create the relation. */
+	namespaceId =
+		RangeVarGetAndCheckCreationNamespace(newPartName, NoLock, &existingRelid);
+	if (OidIsValid(existingRelid))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_TABLE),
+				errmsg("relation \"%s\" already exists", newPartName->relname));
+
+	/* Create the relation. */
+	newRelId = heap_create_with_catalog(newPartName->relname,
+										namespaceId,
+										modelRel->rd_rel->reltablespace,
+										InvalidOid,
+										InvalidOid,
+										InvalidOid,
+										ownerId,
+										relamId,
+										descriptor,
+										NIL,
+										RELKIND_RELATION,
+										newPartName->relpersistence,
+										false,
+										false,
+										ONCOMMIT_NOOP,
+										(Datum) 0,
+										true,
+										allowSystemTableMods,
+										false,
+										InvalidOid,
+										NULL);
+
+	/*
+	 * We must bump the command counter to make the newly-created relation
+	 * tuple visible for opening.
+	 */
+	CommandCounterIncrement();
+
+	/*
+	 * Open the new partition with no lock, because we already have
+	 * AccessExclusiveLock placed there after creation.
+	 */
+	newRel = table_open(newRelId, NoLock);
+
+	/* Find or create work queue entry for newly created table. */
+	new_partrel_tab = ATGetQueueEntry(wqueue, newRel);
+
+	/*
+	 * We intended to create the partition with the same persistence as the
+	 * parent table, but we still need to recheck because that might be
+	 * affected by the search_path.  If the parent is permanent, so must be
+	 * all of its partitions.
+	 */
+	if (modelRel->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
+		newRel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+		ereport(ERROR,
+				errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				errmsg("cannot create a temporary relation as partition of permanent relation \"%s\"",
+					   RelationGetRelationName(modelRel)));
+
+	/* Permanent rels cannot be partitions belonging to temporary parent */
+	if (newRel->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
+		modelRel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+		ereport(ERROR,
+				errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				errmsg("cannot create a permanent relation as partition of temporary relation \"%s\"",
+					   RelationGetRelationName(modelRel)));
+
+	/* Create constraints, default values and generated values */
+	createTableConstraints(wqueue, new_partrel_tab, modelRel, newRel);
+
+	/*
+	 * Need to call CommandCounterIncrement, so fresh relcache entry have newly
+	 * installed constraint info.
+	 */
+	CommandCounterIncrement();
+
+	return newRel;
+}
+
+/*
+ * moveMergedTablesRows: scan partitions to be merged (mergingPartitionsList)
+ * of the partitioned table (rel) and move rows into the new partition
+ * (newPartRel). We also reevaulate check constraints against these rows.
+ */
+static void
+moveMergedTablesRows(List **wqueue, Relation rel,
+					 List *mergingPartitionsList, Relation newPartRel)
+{
+	CommandId	mycid;
+	EState	   *estate;
+	ExprContext *econtext;
+	AlteredTableInfo *tab;
+	ListCell   *ltab;
+
+	/* The FSM is empty, so don't bother using it. */
+	int			ti_options = TABLE_INSERT_SKIP_FSM;
+	BulkInsertState bistate;	/* state of bulk inserts for partition */
+	TupleTableSlot *dstslot;
+
+	/* Find the work queue entry for new partition table: newPartRel. */
+	tab = ATGetQueueEntry(wqueue, newPartRel);
+
+	/* Generate the constraint and default execution states. */
+	estate = CreateExecutorState();
+
+	buildExpressionExecutionStates(tab, newPartRel, estate);
+
+	econtext = GetPerTupleExprContext(estate);
+
+	mycid = GetCurrentCommandId(true);
+
+	/* Prepare a BulkInsertState for table_tuple_insert. */
+	bistate = GetBulkInsertState();
+
+	/* Create necessary tuple slot. */
+	dstslot = table_slot_create(newPartRel, NULL);
+
+	foreach_ptr(RelationData, mergingPartition, mergingPartitionsList)
+	{
+		TupleTableSlot *srcslot;
+		TupleConversionMap *tuple_map;
+		TableScanDesc scan;
+		Snapshot	snapshot;
+
+		/* Create tuple slot for new partition. */
+		srcslot = table_slot_create(mergingPartition, NULL);
+
+		/*
+		 * Map computing for moving attributes of merged partition to new
+		 * partition.
+		 */
+		tuple_map = convert_tuples_by_name(RelationGetDescr(mergingPartition),
+										   RelationGetDescr(newPartRel));
+
+		/* Scan through the rows. */
+		snapshot = RegisterSnapshot(GetLatestSnapshot());
+		scan = table_beginscan(mergingPartition, snapshot, 0, NULL);
+
+		while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot))
+		{
+			TupleTableSlot *insertslot;
+
+			CHECK_FOR_INTERRUPTS();
+
+			if (tuple_map)
+			{
+				/* Need to use map to copy attributes. */
+				insertslot = execute_attr_map_slot(tuple_map->attrMap, srcslot, dstslot);
+			}
+			else
+			{
+				slot_getallattrs(srcslot);
+
+				/* Copy attributes directly. */
+				insertslot = dstslot;
+
+				ExecClearTuple(insertslot);
+
+				memcpy(insertslot->tts_values, srcslot->tts_values,
+					   sizeof(Datum) * srcslot->tts_nvalid);
+				memcpy(insertslot->tts_isnull, srcslot->tts_isnull,
+					   sizeof(bool) * srcslot->tts_nvalid);
+
+				ExecStoreVirtualTuple(insertslot);
+			}
+
+			/*
+			 * Constraints and GENERATED expressions might reference the
+			 * tableoid column, so fill tts_tableOid with the desired
+			 * value.  (We must do this each time, because it gets
+			 * overwritten with newrel's OID during storing.)
+			 */
+			insertslot->tts_tableOid = RelationGetRelid(newPartRel);
+
+			/*
+			 * Now, evaluate any generated expressions whose inputs come from
+			 * the new tuple.  We assume these columns won't reference each
+			 * other, so that there's no ordering dependency.
+			 */
+			evaluateGeneratedExpressionsAndCheckConstraints(tab, newPartRel,
+															insertslot, econtext);
+
+			/* Write the tuple out to the new relation. */
+			table_tuple_insert(newPartRel, insertslot, mycid,
+							   ti_options, bistate);
+
+			ResetExprContext(econtext);
+		}
+
+		table_endscan(scan);
+		UnregisterSnapshot(snapshot);
+
+		if (tuple_map)
+			free_conversion_map(tuple_map);
+
+		ExecDropSingleTupleTableSlot(srcslot);
+	}
+
+	FreeExecutorState(estate);
+	ExecDropSingleTupleTableSlot(dstslot);
+	FreeBulkInsertState(bistate);
+
+	table_finish_bulk_insert(newPartRel, ti_options);
+
+	/*
+	 * We don't need process this newPartRel since we already processed in here,
+	 * so delete the ALTER TABLE queue of it.
+	 */
+	foreach(ltab, *wqueue)
+	{
+		tab = (AlteredTableInfo *) lfirst(ltab);
+		if (tab->relid == RelationGetRelid(newPartRel))
+			*wqueue = list_delete_cell(*wqueue, ltab);
+	}
+}
+
+/*
+ * detachPartitionTable: detach partition "partRel" from partitioned table
+ * "rel" with default partition identifier "defaultPartOid"
+ */
+static void
+detachPartitionTable(Relation partRel, Relation rel, Oid defaultPartOid)
+{
+	/* Remove the pg_inherits row first. */
+	RemoveInheritance(partRel, rel, false);
+
+	/*
+	 * Detaching the partition might involve TOAST table access, so ensure we
+	 * have a valid snapshot.
+	 */
+	PushActiveSnapshot(GetTransactionSnapshot());
+
+	/* Do the final part of detaching. */
+	DetachPartitionFinalize(rel, partRel, false, defaultPartOid);
+
+	PopActiveSnapshot();
+}
+
+/*
+ * ALTER TABLE <name> MERGE PARTITIONS <partition-list> INTO <partition-name>
+ */
+static void
+ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
+					  PartitionCmd *cmd, AlterTableUtilityContext *context)
+{
+	Relation	newPartRel;
+	List	   *mergingPartitionsList = NIL;
+	Oid			defaultPartOid;
+	Oid			existingRelid;
+	Oid			ownerId = InvalidOid;
+	Oid			save_userid;
+	int			save_sec_context;
+	int			save_nestlevel;
+
+	/*
+	 * Lock all merged partitions, check them and create list with partitions
+	 * contexts.
+	 */
+	foreach_node(RangeVar, name, cmd->partlist)
+	{
+		Relation	mergingPartition;
+
+		/*
+		 * We are going to detach and remove this partition: need to use
+		 * exclusive lock for preventing DML-queries to the partition.
+		 */
+		mergingPartition = table_openrv(name, AccessExclusiveLock);
+
+		if (OidIsValid(ownerId))
+		{
+			/* Do the partitions being merged have different owners? */
+			if (ownerId != mergingPartition->rd_rel->relowner)
+				ereport(ERROR,
+						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						errmsg("partitions being merged have different owners"));
+		}
+		else
+			ownerId = mergingPartition->rd_rel->relowner;
+
+		/* Store a next merging partition into the list. */
+		mergingPartitionsList = lappend(mergingPartitionsList,
+										mergingPartition);
+	}
+
+	/*
+	 * Look up existing relation by new partition name, check we have
+	 * permission to create there, lock it against concurrent drop, and mark
+	 * stmt->relation as RELPERSISTENCE_TEMP if a temporary namespace is
+	 * selected.
+	 */
+	cmd->name->relpersistence = rel->rd_rel->relpersistence;
+	RangeVarGetAndCheckCreationNamespace(cmd->name, NoLock, &existingRelid);
+
+	/*
+	 * Check if this name is already taken.  This helps us to detect the
+	 * situation when one of the merging partitions has the same name as the
+	 * new partition.  Otherwise, this would fail later on anyway but catching
+	 * this here allows us to emit a nicer error message.
+	 */
+	if (OidIsValid(existingRelid))
+	{
+		Relation	sameNamePartition = NULL;
+
+		foreach_ptr(RelationData, mergingPartition, mergingPartitionsList)
+		{
+			if (RelationGetRelid(mergingPartition) == existingRelid)
+			{
+				sameNamePartition = mergingPartition;
+				break;
+			}
+		}
+
+		if (sameNamePartition)
+		{
+			/*
+			 * The new partition has the same name as one of merging
+			 * partitions.
+			 */
+			char		tmpRelName[NAMEDATALEN];
+
+			/* Generate temporary name. */
+			sprintf(tmpRelName, "merge-%u-%X-tmp", RelationGetRelid(rel), MyProcPid);
+
+			/*
+			 * Rename the existing partition with a temporary name, leaving it
+			 * free for the new partition.  We don't need to care about this
+			 * in the future because we're going to eventually drop the
+			 * existing partition anyway.
+			 */
+			RenameRelationInternal(RelationGetRelid(sameNamePartition),
+								   tmpRelName, false, false);
+
+			/*
+			 * We must bump the command counter to make the new partition
+			 * tuple visible for rename.
+			 */
+			CommandCounterIncrement();
+		}
+		else
+		{
+			ereport(ERROR,
+					errcode(ERRCODE_DUPLICATE_TABLE),
+					errmsg("relation \"%s\" already exists", cmd->name->relname));
+		}
+	}
+
+	/* Detach all merged partitions. */
+	defaultPartOid =
+		get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true));
+	foreach_ptr(RelationData, mergingPartition, mergingPartitionsList)
+	{
+		detachPartitionTable(mergingPartition, rel, defaultPartOid);
+	}
+
+	/* Create table for new partition, use partitioned table as model. */
+	Assert(OidIsValid(ownerId));
+	newPartRel = createPartitionTable(wqueue, cmd->name, rel, ownerId);
+
+	/*
+	 * Switch to the table owner's userid, so that any index functions are run
+	 * as that user.  Also lock down security-restricted operations and
+	 * arrange to make GUC variable changes local to this command.
+	 *
+	 * Need to do it after determine namespace in createPartitionTable call.
+	 */
+	GetUserIdAndSecContext(&save_userid, &save_sec_context);
+	SetUserIdAndSecContext(ownerId,
+						   save_sec_context | SECURITY_RESTRICTED_OPERATION);
+	save_nestlevel = NewGUCNestLevel();
+	RestrictSearchPath();
+
+	/* Copy data from merged partitions to new partition. */
+	moveMergedTablesRows(wqueue, rel, mergingPartitionsList, newPartRel);
+
+	/* Drop the current partitions before attaching the new one. */
+	foreach_ptr(RelationData, mergingPartition, mergingPartitionsList)
+	{
+		ObjectAddress object;
+
+		/* Get relation id before table_close() call. */
+		object.objectId = RelationGetRelid(mergingPartition);
+		object.classId = RelationRelationId;
+		object.objectSubId = 0;
+
+		/* Keep the lock until commit. */
+		table_close(mergingPartition, NoLock);
+
+		performDeletion(&object, DROP_RESTRICT, 0);
+	}
+	list_free(mergingPartitionsList);
+
+	/*
+	 * Attach a new partition to the partitioned table. wqueue = NULL:
+	 * verification for each cloned constraint is not needed.
+	 */
+	attachPartitionTable(NULL, rel, newPartRel, cmd->bound);
+
+	/* Keep the lock until commit. */
+	table_close(newPartRel, NoLock);
+
+	/* Roll back any GUC changes executed by index functions */
+	AtEOXact_GUC(false, save_nestlevel);
+
+	/* Restore userid and security context */
+	SetUserIdAndSecContext(save_userid, save_sec_context);
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 50f53159d5..46bbdcbc74 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -755,7 +755,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	ORDER ORDINALITY OTHERS OUT_P OUTER_P
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
 
@@ -2331,6 +2331,7 @@ partition_cmd:
 
@@ -2345,6 +2346,7 @@ partition_cmd:
 					n->subtype = AT_DetachPartition;
 					cmd->name = $3;
 					cmd->bound = NULL;
+					cmd->partlist = NIL;
 					cmd->concurrent = $4;
 					n->def = (Node *) cmd;
 
@@ -2358,6 +2360,21 @@ partition_cmd:
@@ -2374,6 +2391,7 @@ index_partition_cmd:
 
@@ -17876,6 +17894,7 @@ unreserved_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PATH
@@ -18503,6 +18522,7 @@ bare_label_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PATH

+/*
+ * checkPartition

 				/*
 
 						/*
 
index 2c0b4f28c1..64248b5b1f 100644

 
index ba12678d1c..b8e2a679cd 100644
@@ -969,8 +969,10 @@ typedef struct PartitionRangeDatum

index 0000000000..064206b973
@@ -0,0 +1,1167 @@
+INSERT INTO tp_0_1(i, t, b) VALUES(0, DEFAULT, 1);
+INSERT INTO tp_1_2(i, t, b) VALUES(1, DEFAULT, 2);
+INSERT INTO t(i, t, b) VALUES(1, DEFAULT, 3);
+NOTICE:  trigger(t) called: action = INSERT, when = BEFORE, level = ROW
+SELECT tableoid::regclass, * FROM t ORDER BY b;
+ tableoid | i |       t        | b |     d      
+----------+---+----------------+---+------------
+ tp_0_1   | 0 | default_tp_0_1 | 1 | 01-01-2022
+ tp_0_1   | 1 | default_tp_1_2 | 2 | 01-01-2022
+ tp_0_1   | 1 | default_t      | 3 | 01-01-2022
 
index 0000000000..a832eb855e
@@ -0,0 +1,770 @@
+INSERT INTO tp_0_1(i, t, b) VALUES(0, DEFAULT, 1);
+INSERT INTO tp_1_2(i, t, b) VALUES(1, DEFAULT, 2);
+INSERT INTO t(i, t, b) VALUES(1, DEFAULT, 3);
+SELECT tableoid::regclass, * FROM t ORDER BY b;

From a31e45df8192e8b12d7c53119fa3f1afc771a678 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:58:09 +0300
Subject: [PATCH v44 2/2] Implement ALTER TABLE ... SPLIT PARTITION ... command

 doc/src/sgml/ref/alter_table.sgml             |   94 +-
 src/backend/commands/tablecmds.c              |  450 ++++
 src/test/regress/expected/partition_split.out | 1878 +++++++++++++++++
 src/test/regress/sql/partition_split.sql      | 1165 ++++++++++
 20 files changed, 4662 insertions(+), 19 deletions(-)

index 62c2bb54ea..41e477eddf 100644
@@ -4461,6 +4461,25 @@ ALTER TABLE measurement
 
index 855d2ca44d..fc7ba5e63c 100644
@@ -1150,6 +1154,73 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+      When partition is split, any individual objects belonging to this
+      partition, such as constraints or statistics will be dropped. This ccurs
+      because <command>ALTER TABLE SPLIT PARTITION</command> uses the partitioned table itself
+      as the template to define these objects.
 
@@ -1244,7 +1315,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1488,7 +1560,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1921,6 +1993,24 @@ ALTER TABLE measurement
 
+  <para>
index 190ce952ad..f0cb66b63a 100644
@@ -742,6 +742,9 @@ static char GetAttributeStorage(Oid atttypid, const char *storagemode);

 /* ----------------------------------------------------------------
  *		DefineRelation
@@ -4840,6 +4843,10 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = AccessExclusiveLock;
 				break;
 
@@ -5280,6 +5287,11 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
@@ -5684,6 +5696,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 			ATExecMergePartitions(wqueue, tab, rel, (PartitionCmd *) cmd->def,
 								  context);
@@ -6726,6 +6746,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -22849,3 +22871,431 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	/* Restore userid and security context */
 	SetUserIdAndSecContext(save_userid, save_sec_context);
+	AlteredTableInfo *tab;		/* structore with generated column expressions
+								 * and check constraint expresssions. */
+deleteSplitPartitionContext(SplitPartitionContext *pc, List **wqueue, int ti_options)
+	ListCell   *ltab;
+	 * We don't need process this pc->partRel so delete the ALTER TABLE queue
+	 * of it.
+	foreach(ltab, *wqueue)
+		AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab);
+		if (tab->relid == RelationGetRelid(pc->partRel))
+			*wqueue = list_delete_cell(*wqueue, ltab);
+moveSplitTableRows(List **wqueue, Relation rel, Relation splitRel, List *partlist, List *newPartRels, Oid defaultPartOid)
+		/* Find the work queue entry for new partition table: newPartRel. */
+		pc->tab = ATGetQueueEntry(wqueue, pc->partRel);
+		buildExpressionExecutionStates(pc->tab, pc->partRel, estate);
+		/* Find the work queue entry for default partition table. */
+		defaultPartCtx->tab = ATGetQueueEntry(wqueue, defaultPartCtx->partRel);
+		 * Constraints and GENERATED expressions might reference the tableoid
+		 * column, so fill tts_tableOid with the desired value. (We must do
+		 * this each time, because it gets overwritten with newrel's OID during
+		 * storing.)
+		insertslot->tts_tableOid = RelationGetRelid(pc->partRel);
+		 * Now, evaluate any generated expressions whose inputs come from
+		 * the new tuple.  We assume these columns won't reference each
+		 * other, so that there's no ordering dependency.
+		evaluateGeneratedExpressionsAndCheckConstraints(pc->tab, pc->partRel,
+														insertslot, econtext);
+		deleteSplitPartitionContext(spc, wqueue, ti_options);
+		deleteSplitPartitionContext(defaultPartCtx, wqueue, ti_options);
+	detachPartitionTable(splitRel, rel, defaultPartOid);
+		newPartRel = createPartitionTable(wqueue, sps->name, rel,
+	moveSplitTableRows(wqueue, rel, splitRel, cmd->partlist, newPartRels, defaultPartOid);
+	/* Roll back any GUC changes executed by index functions. */
+	/* Restore userid and security context. */
index 46bbdcbc74..aed79b63da 100644
@@ -257,6 +257,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -640,6 +641,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -770,7 +773,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -2321,6 +2324,23 @@ alter_table_cmds:
 			| alter_table_cmds ',' alter_table_cmd	{ $$ = lappend($1, $3); }
 		;
 
+partitions_list:
@@ -2365,6 +2385,20 @@ partition_cmd:
@@ -17963,6 +17997,7 @@ unreserved_keyword:
@@ -18603,6 +18638,7 @@ bare_label_keyword:
 
+/*

 
 /*
  * transformPartitionCmd

 
+/*
 
+/*
 
 	return buf->data;
 }
+
+/*
index 64248b5b1f..c0e918951a 100644
 
 	/*
 
index b8e2a679cd..01aa4b2e64 100644
@@ -963,15 +963,26 @@ typedef struct PartitionRangeDatum
 
+/*

 
index 0000000000..eaccc46872
@@ -0,0 +1,1878 @@
+INSERT INTO tp_x(i, t, b) VALUES(0, DEFAULT, 1);
+INSERT INTO tp_x(i, t, b) VALUES(1, DEFAULT, 2);
+INSERT INTO t(i, t, b) VALUES(1, DEFAULT, 3);
+NOTICE:  trigger(t) called: action = INSERT, when = BEFORE, level = ROW
+SELECT tableoid::regclass, * FROM t ORDER BY b;
+ tableoid | i |      t       | b |     d      
+----------+---+--------------+---+------------
+ tp_0_1   | 0 | default_tp_x | 1 | 01-01-2022
+ tp_x     | 1 | default_tp_x | 2 | 01-01-2022
+ tp_x     | 1 | default_t    | 3 | 01-01-2022
 
index 0000000000..289591d496
@@ -0,0 +1,1165 @@
+INSERT INTO tp_x(i, t, b) VALUES(0, DEFAULT, 1);
+INSERT INTO tp_x(i, t, b) VALUES(1, DEFAULT, 2);
+INSERT INTO t(i, t, b) VALUES(1, DEFAULT, 3);
+SELECT tableoid::regclass, * FROM t ORDER BY b;

---

From: jian he <[email-masked]>
Date: Fri, 13 Jun 2025 14:29:23 +0800

hi.

in doc/src/sgml/ref/alter_table.sgml
<title>Parameters</title> section,
we also need explain
<replaceable class="parameter">partition_name1</replaceable>
and
<replaceable class="parameter">partition_name2</replaceable>
?

+     <para>
+      This form merges several partitions into the one partition of
the target table.
+      Hash-partitioning is not supported.
maybe we can change to
+    <para>
+   This form merges several partitions of the target table into a new
one partition.
+   Hash-partitioned target table is not supported

+      <para>
+       This command acquires an <literal>ACCESS EXCLUSIVE</literal> lock.
+       This is a significant limitation, which limits the usage of this
+       command with large partitioned tables under a high load.
+      </para>
would be better mentioning that the parent table and to be merged
partition will all take
<literal>ACCESS EXCLUSIVE</literal> lock.
for example, other places we have word like:

     <para>
      Attaching a partition acquires a
      <literal>SHARE UPDATE EXCLUSIVE</literal> lock on the parent table,
      in addition to the <literal>ACCESS EXCLUSIVE</literal> locks on the table
      being attached and on the default partition (if any).
     </para>
---------------------------------------------------------------------------------
+        <para>
+         For range- and list-partitioned tables the ranges and lists of values
+         of the merged partitions can be any.
+        </para>
we can change it to
<para>
For range-partitioned and list-partitioned tables, the partition
bounds specification can be arbitrary.
</para>

+        <para>
+         For range-partitioned tables it is necessary that the ranges
+         of the partitions <replaceable
class="parameter">partition_name1</replaceable>,
+         <replaceable class="parameter">partition_name2</replaceable>
[, ...] can
+         be merged into one range without spaces and overlaps
(otherwise an error
+         will be generated).
"without spaces and overlaps" seems not ideal, I think I found out the
perfect word for it: adjacent.
in [1], we have description like:

anyrange -|- anyrange → boolean
Are the ranges adjacent?
[1] https://www.postgresql.org/docs/current/functions-range.html
so we can change the above to

For range-partitioned tables, the ranges of the partitions partition_name1,
partition_name2, [...] must be adjacent in order to be merged. Otherwise, an
error will be raised. The resulting combined range will be the new
partition bound for the partition partition_name.

+      The new partition will have the same table access method as the parent.
+      If the parent table is persistent then the new partition is created
+      persistent.  If the parent table is temporary then the new partition
+      is also created temporary.  The new partition will also be created in
+      the same tablespace as the parent.
+     </para>
we can simplify the above as
" The new partition will inherit the same table access method, persistence
 type, and tablespace as the parent table.
"

+     <para>
+      If merged partitions have different owners, an error will be generated.
+      The owner of the merged partitions will be the owner of the new
partition.
+      It is the user's responsibility to setup <acronym>ACL</acronym> on the
+      new partition.
+     </para>
I think we also need to mention that individual ACL on the merged
partition will be dropped.

We have function signature
static void RemoveInheritance(Relation child_rel, Relation parent_rel,
bool expect_detached)
I found that "child_rel", "parent_rel" naming improves code
readability a lot for partitioned table contexts.
so we can change
+static void
+detachPartitionTable(Relation partRel, Relation rel, Oid defaultPartOid)
to
+static void
+detachPartitionTable(Relation partRel, Relation rel, Oid defaultPartOid)

We can also rename the ATExecMergePartitions argument (Relation rel)
to (Relation parent_rel), I think it will improve code reliability.

The attached patch is mainly documentation refactoring
and renaming function detachPartitionTable argument,
it is based on v44.

---

From: Dmitry Koval <[email-masked]>
Date: Fri, 13 Jun 2025 23:06:55 +0300

Hi!

Additional changes (in attached patch):
a. Added using pull_varattnos to check whether a CHECK constraint 
contains a reference to the tableoid column (only such CHECKs are 
recalculated).
b. Added test for recomputation of stored generated columns.

1.
 >We can perform a preliminary check to determine whether dropping a
 >partition is allowed, and raise an error if it's not. To do it, I
 >invented a new function, performDeletionCheck to verify whether an
 >object can be safely dropped.

Applied. I moved calls performDeletionCheck a bit earlier, right after
detachPartitionTable. Is it ok?

2.

Fixed.

 >the third argument, is_internal should set to true?

Ok.

 >we can add an Assert: Assert(!ccnoinherit);

Ok.

 >+ nnconstraints =
 >+     RelationGetNotNullConstraints(RelationGetRelid(modelRel),
 >+  false, true);
 >as mentioned in above, partitioned table cannot have NO INHERIT
 >constraint, maybe we should set RelationGetNotNullConstraints last
 >argument to false

Ok.

 >please let me know whether this improves code readability

I think these changes can be taken partially.
MERGE PARTITIONS and SPLIT PARTITION commands use the same function
check_two_partitions_bounds_range. For MERGE PARTITIONS we can pass
Oids instead of RangeVars (first_name/second_name arguments).
But for SPLIT PARTITION we cannot do this, because at this stage the
new partitions have not yet been created (there are only their names).
Different realizations of check_two_partitions_bounds_range functions
for MERGE PARTITIONS and SPLIT PARTITION are not very good. I think it's
better not change the check_two_partitions_bounds_range function.

3.

Changed.

4. I don't know English very well, so it's difficult for me to correct
the documentation. Thank you very much for the corrections!

 >partition will all take <literal>ACCESS EXCLUSIVE</literal> lock.

Ok, updated.

Other email notes fixed in patch
 >v44-0001-documentation-refactoring-based-on-v44.no-cfbot

Patch applied.

-- 
With best regards,
Dmitry Koval

Postgres Professional: http://postgrespro.com
From 56230dd7082dd4fda9419c953062fc65e05b591c Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:57:22 +0300
Subject: [PATCH v45 1/2] Implement ALTER TABLE ... MERGE PARTITIONS ...
 command

Fixes (summary information).
Authors: Alexander Korotkov, Tender Wang, Richard Guo, Dagfinn Ilmari Mannsaker
Authors: Fujii Masao, Jian He
Reviewed-by: Alexander Korotkov, Robert Haas, Justin Pryzby, Pavel Borisov
Reviewed-by: Masahiko Sawada
Reported-by: Alexander Lakhin, Justin Pryzby, Kyotaro Horiguchi
Reported-by: Daniel Gustafsson, Tom Lane, Noah Misch
---
 doc/src/sgml/ddl.sgml                         |   19 +
 doc/src/sgml/ref/alter_table.sgml             |  104 +-
 src/backend/catalog/dependency.c              |   50 +
 src/backend/catalog/heap.c                    |    4 +-
 src/backend/catalog/pg_constraint.c           |    2 +-
 src/backend/commands/tablecmds.c              |  887 +++++++++++-
 src/backend/parser/gram.y                     |   22 +-
 src/backend/parser/parse_utilcmd.c            |  143 ++
 src/backend/partitioning/partbounds.c         |  198 ++-
 src/bin/psql/tab-complete.in.c                |   10 +
 src/include/catalog/dependency.h              |    2 +
 src/include/catalog/heap.h                    |    3 +
 src/include/nodes/parsenodes.h                |    5 +-
 src/include/parser/kwlist.h                   |    1 +
 src/include/partitioning/partbounds.h         |    6 +
 .../isolation/expected/partition-merge.out    |  199 +++
 src/test/isolation/isolation_schedule         |    1 +
 src/test/isolation/specs/partition-merge.spec |   54 +
 .../test_ddl_deparse/test_ddl_deparse.c       |    3 +
 src/test/regress/expected/partition_merge.out | 1193 +++++++++++++++++
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/partition_merge.sql      |  792 +++++++++++
 22 files changed, 3671 insertions(+), 29 deletions(-)
 create mode 100644 src/test/isolation/expected/partition-merge.out
 create mode 100644 src/test/isolation/specs/partition-merge.spec
 create mode 100644 src/test/regress/expected/partition_merge.out
 create mode 100644 src/test/regress/sql/partition_merge.sql

index d63f3a621a..e503a06028 100644

 
+   <varlistentry id="sql-altertable-merge-partitions">
+    <term><literal>MERGE PARTITIONS (<replaceable class="parameter">partition_name1</replaceable>, <replaceable class="parameter">partition_name2</replaceable> [, ...]) INTO <replaceable class="parameter">partition_name</replaceable></literal></term>
+
+    <listitem>
+     <para>
+      This form merges several partitions of the target table into a new partition.
+      Hash-partitioned target table is not supported.
+      If <literal>DEFAULT</literal> partition is not in the
+      list of partitions <replaceable class="parameter">partition_name1</replaceable>,
+      <replaceable class="parameter">partition_name2</replaceable> [, ...]:
+      <itemizedlist>
+       <listitem>
+        <para>
+         For range-partitioned tables, the ranges of the partitions
+         <replaceable class="parameter">partition_name1</replaceable>,
+         <replaceable class="parameter">partition_name2</replaceable>, [...]
+         must be adjacent in order to be merged. Otherwise, an error will be
+         raised. The resulting combined range will be the new partition bound
+         for the partition <replaceable class="parameter">partition_name</replaceable>.
+        </para>
+       </listitem>
+       <listitem>
+        <para>
+         For list-partitioned tables, the partition bounds of
+         <replaceable class="parameter">partition_name1</replaceable>,
+         <replaceable class="parameter">partition_name2</replaceable>, [...]
+         are combined to form the new partition bound for
+         <replaceable class="parameter">partition_name</replaceable>.
+        </para>
+       </listitem>
+      </itemizedlist>
+      If <literal>DEFAULT</literal> partition is in the list of partitions <replaceable class="parameter">partition_name1</replaceable>,
+      <replaceable class="parameter">partition_name2</replaceable> [, ...]:
+      <itemizedlist>
+       <listitem>
+        <para>
+         The partition <replaceable class="parameter">partition_name</replaceable>
+         will be the new <literal>DEFAULT</literal> partition of the target table.
+        </para>
+       </listitem>
+       <listitem>
+        <para>
+         The partition bound specifications for all partitions-
+         <replaceable class="parameter">partition_name1</replaceable>,
+         <replaceable class="parameter">partition_name2</replaceable>, [...]
+         can be arbitrary.
+        </para>
+       </listitem>
+      </itemizedlist>
+      The new partition <replaceable class="parameter">partition_name</replaceable>
+      can have the same name as one of the merged partitions. Only simple,
+      non-partitioned partitions can be merged.
+     </para>
+     <para>
+      If merged partitions have different owners, an error will be generated.
+      The owner of the merged partitions will be the owner of the new partition.
+      It is the user's responsibility to setup <acronym>ACL</acronym> on the
+      new partition.
+     </para>
+     <para>
+      The indexes and identity are created later, after moving the data
+      into the new partition.
+      Extended statistics aren't copied from the parent table, for consistency with
+      <command>CREATE TABLE PARTITION OF</command>.
+      The new partition will inherit the same table access method, persistence
+      type, and tablespace as the parent table.
+     </para>
+     <para>
+      When partitions are merged, any individual objects belonging to those
+      partitions, such as constraints or statistics will be dropped. This occurs
+      because <command>ALTER TABLE MERGE PARTITIONS</command> uses the partitioned table itself as the
+      template to define these objects.
+     </para>
+     <para>
+      If merged partitions have some objects dependent on them, the command can
+      not be done (<literal>CASCADE</literal> is not used, an error will be returned).
+     </para>
+     <note>
+      <para>
+       Merging partitions acquires a <literal>ACCESS EXCLUSIVE</literal> lock on
+       the parent table, in addition to the <literal>ACCESS EXCLUSIVE</literal>
+       locks on the tables being merged and on the default partition (if any).
+      </para>
+     </note>
+    </listitem>
+   </varlistentry>
+
   </variablelist>
   </para>
 
   <para>
 
+  <para>
 
  <refsect1>
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 18316a3968..5afc493820 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -319,6 +319,56 @@ performDeletion(const ObjectAddress *object,
 	table_close(depRel, RowExclusiveLock);
 }
 
+/*
+ * performDeletionCheck: Check whether a specific object can be safely deleted.
+ * This function does not perform any deletion; instead, it raises an error
+ * if the object cannot be deleted due to existing dependencies.
+ *
+ * It can be useful when you need delete some objects later.  See comments in
+ * performDeletion too.
+ * The behavior must specified as DROP_RESTRICT.
+ */
+void
+performDeletionCheck(const ObjectAddress *object,
+					 DropBehavior behavior, int flags)
+{
+	Relation	depRel;
+	ObjectAddresses *targetObjects;
+
+	Assert(behavior == DROP_RESTRICT);
+
+	depRel = table_open(DependRelationId, RowExclusiveLock);
+
+	AcquireDeletionLock(object, 0);
+
+	/*
+	 * Construct a list of objects we want delete later (ie, the given object plus
+	 * everything directly or indirectly dependent on it).
+	 */
+	targetObjects = new_object_addresses();
+
+	findDependentObjects(object,
+						 DEPFLAG_ORIGINAL,
+						 flags,
+						 NULL,	/* empty stack */
+						 targetObjects,
+						 NULL,	/* no pendingObjects */
+						 &depRel);
+
+	/*
+	 * Check if deletion is allowed.
+	 */
+	reportDependentObjects(targetObjects,
+						   behavior,
+						   flags,
+						   object);
+
+	/* And clean up */
+	free_object_addresses(targetObjects);
+
+	table_close(depRel, RowExclusiveLock);
+}
+
 /*
  * performMultipleDeletions: Similar to performDeletion, but act on multiple
  * objects at once.
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index fbaed5359a..571b6b0574 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -105,8 +105,6 @@ static void RelationRemoveInheritance(Oid relid);
 static Oid	StoreRelCheck(Relation rel, const char *ccname, Node *expr,
 						  bool is_enforced, bool is_validated, bool is_local,
 						  int16 inhcount, bool is_no_inherit, bool is_internal);
-static void StoreConstraints(Relation rel, List *cooked_constraints,
-							 bool is_internal);
 static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
 										bool allow_merge, bool is_local,
 										bool is_enforced,
@@ -2296,7 +2294,7 @@ StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum,
  * expressions can be added later, by direct calls to StoreAttrDefault
  * and StoreRelCheck (see AddRelationNewConstraints()).
  */
-static void
+void
 StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal)
 {
 	int			numchecks = 0;
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 2d5ac1ea81..1f948876d9 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -875,7 +875,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
 															 false)));
 			constr->is_enforced = true;
 			constr->skip_validation = !conForm->convalidated;
-			constr->initially_valid = true;
+			constr->initially_valid = conForm->convalidated;
 			constr->is_no_inherit = conForm->connoinherit;
 			notnulls = lappend(notnulls, constr);
 		}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ea96947d81..7e7703c799 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -740,6 +740,8 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, const char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);

 
 
+/*

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -22035,3 +22072,819 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+
+/*
+		 * Partitioned table can not have NO INHERIT check constraint (see
+		 * StoreRelCheck function).
+		Assert(!ccnoinherit);
+	foreach_ptr(CookedConstraint, ccon, cookedConstraints)
+			Bitmapset	  *attnums = NULL;
+			pull_varattnos((Node *) ccon->expr, 1, &attnums);
+			 * Add check only if it contains tableoid
+			 * (TableOidAttributeNumber).
+			if (bms_is_member(TableOidAttributeNumber - FirstLowInvalidHeapAttributeNumber,
+							  attnums))
+		 * The "include_noinh" argument is false because a partitioned table
+		 * cannot have NO INHERIT constraint.
+													  false, false);
+ * detachPartitionTable: detach partition "child_rel" from partitioned table
+ * "parent_rel" with default partition identifier "defaultPartOid"
+detachPartitionTable(Relation parent_rel, Relation child_rel, Oid defaultPartOid)
+	RemoveInheritance(child_rel, parent_rel, false);
+	DetachPartitionFinalize(parent_rel, child_rel, false, defaultPartOid);
+								   tmpRelName, true, false);
+	 * Detach all merged partitions.
+	 * Perform a preliminary check to determine whether it's safe to drop all
+	 * merging partitions before we actually do so later. After merging rows
+	 * into the new partitions via moveMergedTablesRows, all old partitions need
+	 * be dropped. However, since the drop behavior is DROP_RESTRICT and the
+	 * merge process (moveMergedTablesRows) can be time-consuming, performing an
+	 * early check on the drop eligibility of old partitions is preferable.
+		detachPartitionTable(rel, mergingPartition, defaultPartOid);
+		/* Get oid of the later to be dropped relation */
+		performDeletionCheck(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);

 
 
index 62015431fd..da29198644 100644
@@ -3488,6 +3491,133 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
 }

+/*
+ * checkPartition
+	defaultPartOid = get_default_oid_from_partdesc(RelationGetPartitionDesc(parent, true));
@@ -3765,6 +3895,19 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,

 				/*
index 4bdc2941ef..2664c7b709 100644
 
 						/*
@@ -4977,3 +4978,196 @@ satisfies_hash_partition(PG_FUNCTION_ARGS)
 
+ * This is a helper function for calculate_partition_bound_for_merge().
+ * second_bound. These bounds should be equal.
+	if (cmpval)
+ * Returns the PartitionBoundSpec for the partition with the given OID partOid.
+get_partition_bound_spec(Oid partOid)
+		elog(ERROR, "cache lookup failed for relation %u", partOid);
+		elog(ERROR, "partition bound for relation %u is null",
+			 partOid);
+		elog(ERROR, "expected PartitionBoundSpec for relation %u",
+			 partOid);
+				foreach_oid(partoid, partOids)
+					bound = get_partition_bound_spec(partoid);
+					i = foreach_current_index(partoid);
+				foreach_oid(partoid, partOids)
+					bound = get_partition_bound_spec(partoid);
 
+	/* ALTER TABLE <name> MERGE PARTITIONS ( */
+	else if (Matches("ALTER", "TABLE", MatchAny, "MERGE", "PARTITIONS", "("))
+	{
+		set_completion_reference(prev4_wd);
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_partition_of_table);
+	}
+	else if (Matches("ALTER", "TABLE", MatchAny, "MERGE", "PARTITIONS", "(*)"))
+		COMPLETE_WITH("INTO");
+
 	/* ALTER TABLE <name> OF */
 	else if (Matches("ALTER", "TABLE", MatchAny, "OF"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_composite_datatypes);
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 0ea7ccf524..f54233499b 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -107,6 +107,8 @@ extern void ReleaseDeletionLock(const ObjectAddress *object);
 extern void performDeletion(const ObjectAddress *object,
 							DropBehavior behavior, int flags);
 
+extern void performDeletionCheck(const ObjectAddress *object,
+								 DropBehavior behavior, int flags);
 extern void performMultipleDeletions(const ObjectAddresses *objects,
 									 DropBehavior behavior, int flags);

 

index 0000000000..4bfc4f7b5f
@@ -0,0 +1,1193 @@
+-- Test for recomputation of stored generated columns.
+CREATE TABLE t (i int, tab_id int generated always as (tableoid) stored) PARTITION BY RANGE (i);
+ALTER TABLE t ADD CONSTRAINT cc CHECK(tableoid <> 123456789);
+-- Should be 0 because partition identifier for row with i=0 is different from
+-- partition identifier for row with i=1.
+SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i = 1);
+ count 
+-------
+     0
+-- "tab_id" column (stored generated column) with "tableoid" attribute requires
+-- recomputation here.
+-- Should be 1 because partition identifier for row with i=0 is the same as
+-- partition identifier for row with i=1.
+SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i = 1);
+ count 
+-------
+     1
 
index 0000000000..38cf4afbf1
@@ -0,0 +1,792 @@
+-- Test for recomputation of stored generated columns.
+CREATE TABLE t (i int, tab_id int generated always as (tableoid) stored) PARTITION BY RANGE (i);
+ALTER TABLE t ADD CONSTRAINT cc CHECK(tableoid <> 123456789);
+-- Should be 0 because partition identifier for row with i=0 is different from
+-- partition identifier for row with i=1.
+SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i = 1);
+-- "tab_id" column (stored generated column) with "tableoid" attribute requires
+-- recomputation here.
+-- Should be 1 because partition identifier for row with i=0 is the same as
+-- partition identifier for row with i=1.
+SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i = 1);

From 2aec1a86faeba0e14c9b86163445b285fe6f898d Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:58:09 +0300
Subject: [PATCH v45 2/2] Implement ALTER TABLE ... SPLIT PARTITION ... command

Fixes (summary information).
Authors: Alexander Korotkov, Tender Wang, Richard Guo, Dagfinn Ilmari Mannsaker
Authors: Fujii Masao
Reviewed-by: Alexander Korotkov, Robert Haas, Justin Pryzby, Pavel Borisov
Reviewed-by: Masahiko Sawada
Reported-by: Alexander Lakhin, Justin Pryzby, Kyotaro Horiguchi
Reported-by: Daniel Gustafsson, Tom Lane, Noah Misch
---
 doc/src/sgml/ddl.sgml                         |   19 +
 doc/src/sgml/ref/alter_table.sgml             |   92 +-
 src/backend/commands/tablecmds.c              |  463 ++++
 src/backend/parser/gram.y                     |   38 +-
 src/backend/parser/parse_utilcmd.c            |   56 +-
 src/backend/partitioning/partbounds.c         |  684 +++++-
 src/backend/utils/adt/ruleutils.c             |   18 +
 src/bin/psql/tab-complete.in.c                |   10 +-
 src/include/nodes/parsenodes.h                |   16 +-
 src/include/parser/kwlist.h                   |    1 +
 src/include/partitioning/partbounds.h         |    4 +
 src/include/utils/ruleutils.h                 |    2 +
 .../isolation/expected/partition-split.out    |  190 ++
 src/test/isolation/isolation_schedule         |    1 +
 src/test/isolation/specs/partition-split.spec |   54 +
 .../test_ddl_deparse/test_ddl_deparse.c       |    3 +
 src/test/regress/expected/partition_split.out | 1905 +++++++++++++++++
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/partition_split.sql      | 1188 ++++++++++
 src/tools/pgindent/typedefs.list              |    2 +
 20 files changed, 4725 insertions(+), 23 deletions(-)
 create mode 100644 src/test/isolation/expected/partition-split.out
 create mode 100644 src/test/isolation/specs/partition-split.spec
 create mode 100644 src/test/regress/expected/partition_split.out
 create mode 100644 src/test/regress/sql/partition_split.sql

index e503a06028..daadb8326b 100644
@@ -1150,6 +1154,71 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+      This form splits a single partition of the target table into a new
+      partitions. Hash-partitioned target table is not supported. Bounds of new
+      partitions should not overlap with new and existing partitions
+      (except <replaceable class="parameter">partition_name</replaceable>).
+      New partitions will inherit the same table access method, persistence
+      type, and tablespace as the parent table.
+       Split partition acquires a <literal>ACCESS EXCLUSIVE</literal> lock on
+       the parent table, in addition to the <literal>ACCESS EXCLUSIVE</literal>
+       lock on the table being split.
 
@@ -1244,7 +1313,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1488,7 +1558,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1921,6 +1991,24 @@ ALTER TABLE measurement
 
+  <para>
index 7e7703c799..f5cd09d3ed 100644

 
@@ -22888,3 +22910,444 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	/* Restore userid and security context. */
+	detachPartitionTable(rel, splitRel, defaultPartOid);
+	 * Perform a preliminary check to determine whether it's safe to drop all
+	 * merging partitions before we actually do so later. After merging rows
+	 * into the new partitions via moveMergedTablesRows, all old partitions need
+	 * be dropped. However, since the drop behavior is DROP_RESTRICT and the
+	 * merge process (moveMergedTablesRows) can be time-consuming, performing an
+	 * early check on the drop eligibility of old partitions is preferable.
+	performDeletionCheck(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
+		RenameRelationInternal(splitRelOid, tmpRelName, true, false);

 
+partitions_list:
index da29198644..cb356cabc9 100644
 
+/*
+	check_partitions_for_split(parent, splitPartOid, partcmd->partlist, cxt->pstate);
@@ -3887,7 +3918,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3895,6 +3926,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3903,7 +3935,11 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
@@ -4338,13 +4374,13 @@ setSchemaName(const char *context_schema, char **stmt_schema_name)
 
 /*
  * transformPartitionCmd
 
@@ -4353,9 +4389,9 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 
@@ -4363,7 +4399,7 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 			 * A partitioned index cannot have a partition bound set.  ALTER
 			 * INDEX prevents that with its grammar, but not ALTER TABLE.
 			 */
-			if (cmd->bound != NULL)
+			if (bound != NULL)
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("\"%s\" is not a partitioned table",
diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index 2664c7b709..f77f3d6a02 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -4984,15 +4984,21 @@ satisfies_hash_partition(PG_FUNCTION_ARGS)
  *
  * (function for BY RANGE partitioning)
  *
- * This is a helper function for calculate_partition_bound_for_merge().
+ * This is a helper function for check_partitions_for_split() and
+ * calculate_partition_bound_for_merge().
  * This function compares upper bound of first_bound and lower bound of
- * second_bound. These bounds should be equal.
+ * second_bound. These bounds should be equal except when
+ * "defaultPart == true" (this means that one of split partitions is DEFAULT).
+ * In this case upper bound of first_bound can be less than lower bound of
+ * second_bound because space between these bounds will be included in
+ * DEFAULT partition.
  *
  * parent:			partitioned table
  * first_name:		name of first partition
  * first_bound:		bound of first partition
  * second_name:		name of second partition
  * second_bound:	bound of second partition
+ * defaultPart:		true if one of split partitions is DEFAULT
  * pstate:			pointer to ParseState struct for determining error position
  */
 static void
@@ -5001,6 +5007,7 @@ check_two_partitions_bounds_range(Relation parent,
 								  PartitionBoundSpec *first_bound,
 								  RangeVar *second_name,
 								  PartitionBoundSpec *second_bound,
+								  bool defaultPart,
 								  ParseState *pstate)
 {
 	PartitionKey key = RelationGetPartitionKey(parent);
@@ -5022,7 +5029,7 @@ check_two_partitions_bounds_range(Relation parent,
 								  key->partcollation,
 								  second_lower->datums, second_lower->kind,
 								  false, first_upper);
-	if (cmpval)
+	if ((!defaultPart && cmpval) || (defaultPart && cmpval < 0))
 	{
 		PartitionRangeDatum *datum = linitial(second_bound->lowerdatums);
 
@@ -5133,7 +5140,7 @@ calculate_partition_bound_for_merge(Relation parent,
 													  (PartitionBoundSpec *) list_nth(bounds, prev_index),
 													  (RangeVar *) list_nth(partNames, index),
 													  (PartitionBoundSpec *) list_nth(bounds, index),
-													  pstate);
+													  false, pstate);
 				}
 
 				/*
@@ -5171,3 +5178,672 @@ calculate_partition_bound_for_merge(Relation parent,
 				 (int) key->strategy);
 	}
 }
+
+/*
+		PartitionBoundSpec *split_spec = get_partition_bound_spec(splitPartOid);
+													   splitPartOid, first, last,
 
 	return buf->data;
 }
+
+/*
 
 	/*

+/*
index 690d259619..45b1fa1de8 100644
@@ -143,6 +143,10 @@ extern int	partition_range_datum_bsearch(FmgrInfo *partsupfunc,

 
index 0000000000..aa34440c65
@@ -0,0 +1,1905 @@
+-- Test for recomputation of stored generated columns.
+CREATE TABLE t (i int, tab_id int generated always as (tableoid) stored) PARTITION BY RANGE (i);
+ALTER TABLE t ADD CONSTRAINT cc CHECK(tableoid <> 123456789);
+-- Should be 1 because partition identifier for row with i=0 is the same as
+-- partition identifier for row with i=1.
+SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i = 1);
+ count 
+-------
+     1
+-- "tab_id" column (stored generated column) with "tableoid" attribute requires
+-- recomputation here.
+-- Should be 0 because partition identifier for row with i=0 is different from
+-- partition identifier for row with i=1.
+SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i = 1);
+ count 
+-------
+     0
 
index 0000000000..f816c91597
@@ -0,0 +1,1188 @@
+-- Test for recomputation of stored generated columns.
+CREATE TABLE t (i int, tab_id int generated always as (tableoid) stored) PARTITION BY RANGE (i);
+ALTER TABLE t ADD CONSTRAINT cc CHECK(tableoid <> 123456789);
+-- Should be 1 because partition identifier for row with i=0 is the same as
+-- partition identifier for row with i=1.
+SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i = 1);
+-- "tab_id" column (stored generated column) with "tableoid" attribute requires
+-- recomputation here.
+-- Should be 0 because partition identifier for row with i=0 is different from
+-- partition identifier for row with i=1.
+SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i = 1);

---

From: jian he <[email-masked]>
Date: Mon, 16 Jun 2025 13:52:45 +0800

hi.

static void checkPartition(Relation rel, Oid partRelOid)
function name checkPartition is not ideal, maybe we can change it to
CheckPartitionForMerge or MergePartitionCheck.
(attached v45-002 is error message refactoring for checkPartition,
I didn't change the name though.)

For the command:
ALTER TABLE pk MERGE PARTITIONS (pk_1, pk_2) INTO pk_1;
Acquiring AccessExclusiveLock on the partitions to be merged
(pk_1, pk_2) during transformPartitionCmdForMerge should be fine, IMHO.
Here’s why:

* The merged partitions (pk_1, pk_2) will be dropped in the end, so acquiring
AccessExclusiveLock is unavoidable for ALTER TABLE MERGE PARTITIONS.
* Taking an AccessShareLock first, then later acquiring AccessExclusiveLock
in ATExecMergePartitions unnecessarily wastes resources.
(acquire two locks, one stronger should be enough)
* Acquiring AccessExclusiveLock first helps avoid potential anomalies
caused by concurrent operations.

The attached patch refactors transformPartitionCmdForMerge and
ATExecMergePartitions based on the idea of acquiring AccessExclusiveLock on the
to be merged partitions during transformPartitionCmdForMerge

+ * Callback allows caller to check permissions or acquire additional locks
+ * prior to grabbing the relation lock.
Please see the above comments in RangeVarGetRelidExtended.

+ /*
+ * Search DEFAULT partition in the list. Lock partitions before
+ * calculating the boundary for resulting partition.
+ */
+ partOid = RangeVarGetRelid(name, AccessShareLock, false);
so the above transformPartitionCmdForMerge does not check if the
currently user have permission
or not, directly take a lock on RangeVar, name, which is a bug, we should
first do permission check then acquire a lock.

---

From: jian he <[email-masked]>
Date: Mon, 16 Jun 2025 17:33:52 +0800

for v45.

+ if (bms_is_member(TableOidAttributeNumber -
FirstLowInvalidHeapAttributeNumber,

we need to expand the virtual generated column here,
otherwise, bms_is_member would  be not correct.
consider case like:

CREATE TABLE pp (f1 INT, f2 INT generated always as (f1 +
tableoid::int)) PARTITION BY RANGE (f1);
CREATE TABLE pp_1 (f2 INT generated always as (f1 + tableoid::int), f1 int);
ALTER TABLE pp ATTACH PARTITION pp_1 FOR VALUES FROM (-1) TO (10);
CREATE TABLE pp_2 (f2 INT generated always as (f1 + tableoid::int), f1 int);
ALTER TABLE pp ATTACH PARTITION pp_2 FOR VALUES FROM (10) TO (20);
ALTER TABLE PP add check (f2 > 0);

ALTER TABLE pp MERGE PARTITIONS (pp_1, pp_2) INTO pp_12;

In this context, the merge partition command  needs to evaluate the constraint
"pp_f2_check" again on pp_12.

attach minor diff fix this problem.

---

From: Dmitry Koval <[email-masked]>
Date: Mon, 16 Jun 2025 23:15:41 +0300

Hi!

1.
 >CheckPartitionForMerge or MergePartitionCheck.

I agree that this name is not ideal. But the "checkPartition" function
is used for both MERGE/SPLIT commands, so the word 'Merge' in the
prefix or suffix of the function name would not be correct ...
May be "checkPartitionForModification" or something like that?

 >ATExecMergePartitions based on the idea of acquiring
 >AccessExclusiveLock on the to be merged partitions during

Thanks, applied.

2.
 >attach minor diff fix this problem.

Added patch and a bit modified test.

-- 
With best regards,
Dmitry Koval

Postgres Professional: http://postgrespro.com
From 1b290e06539f5768962c4457605b24197d0ecf50 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:57:22 +0300
Subject: [PATCH v46 1/2] Implement ALTER TABLE ... MERGE PARTITIONS ...
 command

 src/backend/commands/tablecmds.c              |  906 +++++++++++-
 src/backend/parser/parse_utilcmd.c            |  148 ++
 src/backend/partitioning/partbounds.c         |  199 ++-
 src/test/regress/expected/partition_merge.out | 1234 +++++++++++++++++
 src/test/regress/sql/partition_merge.sql      |  816 +++++++++++
 22 files changed, 3761 insertions(+), 29 deletions(-)

 

   <para>
 
+  <para>
 
  <refsect1>
 
+/*
index ea96947d81..9a9053d325 100644

 
 
+/*

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -22035,3 +22072,838 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+
+/*
+	 * or not via pull_varattnos.
+			Node		*qual;
+			qual = expand_generated_columns_in_expr(ccon->expr, newRel, 1);
+			pull_varattnos(qual, 1, &attnums);
+				newcon->qual = qual;
+ * moveMergedTablesRows: scan partitions to be merged (mergingPartitions)
+					 List *mergingPartitions, Relation newPartRel)
+	foreach_oid(merging_oid, mergingPartitions)
+		 * Partition is already locked in the transformPartitionCmdForMerge
+		 * function.
+		mergingPartition = table_open(merging_oid, NoLock);
+	List	   *mergingPartitions = NIL;
+	 * Check ownership of merged partitions — partitions with different owners
+	 * cannot be merged. Also, collect the OIDs of these partitions during the
+	 * check.
+		 * We are going to detach and remove this partition.  We already took
+		 * AccessExclusiveLock lock on transformPartitionCmdForMerge, so here,
+		 * NoLock is fine.
+		mergingPartition = table_openrv_extended(name, NoLock, false);
+		Assert(CheckRelationLockedByMe(mergingPartition, AccessExclusiveLock, false));
+		mergingPartitions = lappend_oid(mergingPartitions,
+										RelationGetRelid(mergingPartition));
+		Oid		newPartitionOid = InvalidOid;
+		foreach_oid(mergingPartitionOid, mergingPartitions)
+			if (mergingPartitionOid == existingRelid)
+				newPartitionOid = mergingPartitionOid;
+		if (OidIsValid(newPartitionOid))
+			RenameRelationInternal(newPartitionOid, tmpRelName, true, false);
+	foreach_oid(mergingPartitionOid, mergingPartitions)
+		Relation	child_rel;
+		child_rel = table_open(mergingPartitionOid, NoLock);
+		detachPartitionTable(rel, child_rel, defaultPartOid);
+		table_close(child_rel, NoLock);
+	foreach_oid(mergingPartitionOid, mergingPartitions)
+		object.objectId = mergingPartitionOid;
+	moveMergedTablesRows(wqueue, rel, mergingPartitions, newPartRel);
+	foreach_oid(mergingPartitionOid, mergingPartitions)
+		object.objectId = mergingPartitionOid;
+	list_free(mergingPartitions);

 
 
index 62015431fd..f55ad17528 100644
@@ -3488,6 +3491,138 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
 }

+/*
+ * checkPartition
+ * Check whether partRelOid is a leaf partition of the parent table (rel).
+ * Partition with OID partRelOid must be locked before function call.
+ */
+static void
+checkPartition(Relation rel, Oid partRelOid)
+{
+	Relation	partRel;
+
+	partRel = table_open(partRelOid, NoLock);
+
+	if (partRel->rd_rel->relkind != RELKIND_RELATION)
+		ereport(ERROR,
+				errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				errmsg("\"%s\" is not a table", RelationGetRelationName(partRel)),
+				errhint("ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions"));
+
+	if (!partRel->rd_rel->relispartition)
+		ereport(ERROR,
+				errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				errmsg("\"%s\" is not a partition of partitioned table \"%s\"",
+					   RelationGetRelationName(partRel), RelationGetRelationName(rel)),
+				errhint("ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions"));
+
+	if (get_partition_parent(partRelOid, false) != RelationGetRelid(rel))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_TABLE),
+				errmsg("relation \"%s\" is not a partition of relation \"%s\"",
+					   RelationGetRelationName(partRel), RelationGetRelationName(rel)),
+				errhint("ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions"));
+
+	table_close(partRel, NoLock);
+}
+
+/*
+ * transformPartitionCmdForMerge
+ *		Analyze the ALTER TABLE ... MERGE PARTITIONS command
+ *
+ * Does simple checks for merged partitions. Calculates bound of resulting
+ * partition.
+ */
+static void
+transformPartitionCmdForMerge(CreateStmtContext *cxt, PartitionCmd *partcmd)
+{
+	Oid			defaultPartOid;
+	Oid			partOid;
+	Relation	parent = cxt->rel;
+	PartitionKey key;
+	char		strategy;
+	ListCell   *listptr,
+			   *listptr2;
+	bool		isDefaultPart = false;
+	List	   *partOids = NIL;
+
+	key = RelationGetPartitionKey(parent);
+	strategy = get_partition_strategy(key);
+
+	if (strategy == PARTITION_STRATEGY_HASH)
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("partition of hash-partitioned table cannot be merged"));
+
+	/* Is current partition a DEFAULT partition? */
+	defaultPartOid = get_default_oid_from_partdesc(RelationGetPartitionDesc(parent, true));
+
+	foreach(listptr, partcmd->partlist)
+	{
+		RangeVar   *name = (RangeVar *) lfirst(listptr);
+
+		/* Partitions in the list should have different names. */
+		for_each_cell(listptr2, partcmd->partlist, lnext(partcmd->partlist, listptr))
+		{
+			RangeVar   *name2 = (RangeVar *) lfirst(listptr2);
+
+			if (equal(name, name2))
+				ereport(ERROR,
+						errcode(ERRCODE_DUPLICATE_TABLE),
+						errmsg("partition with name \"%s\" is already used", name->relname),
+						parser_errposition(cxt->pstate, name2->location));
+		}
+
+		/*
+		 * Search DEFAULT partition in the list. Open and lock partitions before
+		 * calculating the boundary for resulting partition, we also check for
+		 * ownership along the way.  We need to use AccessExclusiveLock here,
+		 * because these merged partitions will be detached then dropped in
+		 * ATExecMergePartitions.
+		 */
+		partOid = RangeVarGetRelidExtended(name,
+										   AccessExclusiveLock,
+										   false,
+										   RangeVarCallbackOwnsRelation,
+										   NULL);
+
+		if (partOid == defaultPartOid)
+			isDefaultPart = true;
+
+		/*
+		 * Extended check because the same partition can have different names
+		 * (for example, "part_name" and "public.part_name").
+		 */
+		foreach(listptr2, partOids)
+		{
+			Oid		curOid = lfirst_oid(listptr2);
+
+			if (curOid == partOid)
+				ereport(ERROR,
+						errcode(ERRCODE_DUPLICATE_TABLE),
+						errmsg("partition with name \"%s\" is already used", name->relname),
+						parser_errposition(cxt->pstate, name->location));
+		}
+
+		checkPartition(parent, partOid);
+
+		partOids = lappend_oid(partOids, partOid);
+	}
+
+	/* Allocate bound of resulting partition. */
+	Assert(partcmd->bound == NULL);
+	partcmd->bound = makeNode(PartitionBoundSpec);
+
+	/* Fill partition bound. */
+	partcmd->bound->strategy = strategy;
+	partcmd->bound->location = -1;
+	partcmd->bound->is_default = isDefaultPart;
+	if (!isDefaultPart)
+		calculate_partition_bound_for_merge(parent, partcmd->partlist,
+											partOids, partcmd->bound,
+											cxt->pstate);
+}
+
 /*
  * transformAlterTableStmt -
  *		parse analysis for ALTER TABLE
@@ -3765,6 +3900,19 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 				newcmds = lappend(newcmds, cmd);
 				break;

 				/*
index 4bdc2941ef..9f7c01b7fa 100644
 
 						/*
@@ -4977,3 +4978,197 @@ satisfies_hash_partition(PG_FUNCTION_ARGS)
 
+				errmsg("lower bound of partition \"%s\" is not equal to the upper bound of partition \"%s\"",
+				errhint("ALTER TABLE ... MERGE PARTITIONS requires the partition bounds to be adjacent."),

 

 
 
index 0000000000..1cd34fae2d
@@ -0,0 +1,1234 @@
+HINT:  ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions
+-- ERROR:  lower bound of partition "sales_mar2022" is not equal to the upper bound of partition "sales_jan2022"
+ERROR:  lower bound of partition "sales_mar2022" is not equal to the upper bound of partition "sales_jan2022"
+HINT:  ALTER TABLE ... MERGE PARTITIONS requires the partition bounds to be adjacent.
+-- ERROR:  lower bound of partition "sales_jan2022" is not equal to the upper bound of partition "sales_dec2021"
+ERROR:  lower bound of partition "sales_jan2022" is not equal to the upper bound of partition "sales_dec2021"
+HINT:  ALTER TABLE ... MERGE PARTITIONS requires the partition bounds to be adjacent.
+-- ERROR:  "sales_external" is not a partition of partitioned table "sales_list"
+ERROR:  "sales_external" is not a partition of partitioned table "sales_list"
+HINT:  ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions
+-- ERROR:  "sales_external2" is not a partition of partitioned table "sales_list"
+ERROR:  "sales_external2" is not a partition of partitioned table "sales_list"
+HINT:  ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions
+HINT:  ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions
+HINT:  ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions
+-- ERROR:  "t3" is not a partition of partitioned table "t2"
+ERROR:  "t3" is not a partition of partitioned table "t2"
+HINT:  ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions
+-- Test for generated columns (different order of columns in partitioned table
+-- and partitions).
+CREATE TABLE t (i int, g int GENERATED ALWAYS AS (i + tableoid::int)) PARTITION BY RANGE (i);
+CREATE TABLE tp_1 (g int GENERATED ALWAYS AS (i + tableoid::int), i int);
+CREATE TABLE tp_2 (g int GENERATED ALWAYS AS (i + tableoid::int), i int);
+ALTER TABLE t ATTACH PARTITION tp_1 FOR VALUES FROM (-1) TO (10);
+ALTER TABLE t ATTACH PARTITION tp_2 FOR VALUES FROM (10) TO (20);
+ALTER TABLE t ADD CHECK (g > 0);
+ALTER TABLE t ADD CHECK (i > 0);
+INSERT INTO t VALUES (5), (15);
+ALTER TABLE t MERGE PARTITIONS (tp_1, tp_2) INTO tp_12;
+INSERT INTO t VALUES (16);
+-- ERROR:  new row for relation "tp_12" violates check constraint "t_i_check"
+INSERT INTO t VALUES (0);
+ERROR:  new row for relation "tp_12" violates check constraint "t_i_check"
+DETAIL:  Failing row contains (0, virtual).
+-- Should be 3 rows: (5), (15), (16):
+SELECT i FROM t ORDER BY i;
+ i  
+----
+  5
+ 15
+ 16
+-- Should be 1 because for the same tableoid (15 + tableoid) = (5 + tableoid) + 10:
+SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5);
 
index 0000000000..5baba56392
@@ -0,0 +1,816 @@
+-- ERROR:  lower bound of partition "sales_mar2022" is not equal to the upper bound of partition "sales_jan2022"
+-- ERROR:  lower bound of partition "sales_jan2022" is not equal to the upper bound of partition "sales_dec2021"
+-- ERROR:  "sales_external" is not a partition of partitioned table "sales_list"
+-- ERROR:  "sales_external2" is not a partition of partitioned table "sales_list"
+-- ERROR:  "t3" is not a partition of partitioned table "t2"
+-- Test for generated columns (different order of columns in partitioned table
+-- and partitions).
+CREATE TABLE t (i int, g int GENERATED ALWAYS AS (i + tableoid::int)) PARTITION BY RANGE (i);
+CREATE TABLE tp_1 (g int GENERATED ALWAYS AS (i + tableoid::int), i int);
+CREATE TABLE tp_2 (g int GENERATED ALWAYS AS (i + tableoid::int), i int);
+ALTER TABLE t ATTACH PARTITION tp_1 FOR VALUES FROM (-1) TO (10);
+ALTER TABLE t ATTACH PARTITION tp_2 FOR VALUES FROM (10) TO (20);
+ALTER TABLE t ADD CHECK (g > 0);
+ALTER TABLE t ADD CHECK (i > 0);
+INSERT INTO t VALUES (5), (15);
+ALTER TABLE t MERGE PARTITIONS (tp_1, tp_2) INTO tp_12;
+INSERT INTO t VALUES (16);
+-- ERROR:  new row for relation "tp_12" violates check constraint "t_i_check"
+INSERT INTO t VALUES (0);
+-- Should be 3 rows: (5), (15), (16):
+SELECT i FROM t ORDER BY i;
+-- Should be 1 because for the same tableoid (15 + tableoid) = (5 + tableoid) + 10:
+SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5);

From 5411bff81bd8b6240efd06e9a6169e768959fecf Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:58:09 +0300
Subject: [PATCH v46 2/2] Implement ALTER TABLE ... SPLIT PARTITION ... command

 src/backend/parser/parse_utilcmd.c            |   65 +-
 src/test/regress/expected/partition_split.out | 1911 +++++++++++++++++
 20 files changed, 4740 insertions(+), 23 deletions(-)

 

+  <para>
index 9a9053d325..789c6828d9 100644

 
@@ -22907,3 +22929,444 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
+	 * Partition is already locked in the transformPartitionCmdForSplit
+	 * function.
+	splitRel = table_openrv(cmd->name, NoLock);

 
+partitions_list:
index f55ad17528..73315efac8 100644
@@ -3526,6 +3526,46 @@ checkPartition(Relation rel, Oid partRelOid)
 	table_close(partRel, NoLock);
 }
 
+/*
+ * transformPartitionCmdForSplit
+ *		Analyze the ALTER TABLE ... SPLIT PARTITION command
+ *
+ * For each new partition sps->bound is set to the transformed value of bound.
+ * Does checks for bounds of new partitions.
+ */
+static void
+transformPartitionCmdForSplit(CreateStmtContext *cxt, PartitionCmd *partcmd)
+{
+	Relation	parent = cxt->rel;
+	Oid			splitPartOid;
+
+	/* Transform partition bounds for all partitions in the list: */
+	foreach_node(SinglePartitionSpec, sps, partcmd->partlist)
+	{
+		cxt->partbound = NULL;
+		transformPartitionCmd(cxt, sps->bound);
+		/* Assign transformed value of the partition bound. */
+		sps->bound = cxt->partbound;
+	}
+
+	/*
+	 * Open and lock partition, check ownership along the way. We need to use
+	 * AccessExclusiveLock here, because this split partition will be detached
+	 * then dropped in ATExecSplitPartition.
+	 */
+	splitPartOid = RangeVarGetRelidExtended(partcmd->name,
+											AccessExclusiveLock,
+											false,
+											RangeVarCallbackOwnsRelation,
+											NULL);
+
+	checkPartition(parent, splitPartOid);
+
+	/* Then we should check partitions with transformed bounds. */
+	check_partitions_for_split(parent, splitPartOid, partcmd->partlist, cxt->pstate);
+}
+
+
 /*
  * transformPartitionCmdForMerge
  *		Analyze the ALTER TABLE ... MERGE PARTITIONS command
@@ -3892,7 +3932,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 				{
 					PartitionCmd *partcmd = (PartitionCmd *) cmd->def;
 
@@ -3900,6 +3940,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3908,7 +3949,11 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
@@ -4343,13 +4388,13 @@ setSchemaName(const char *context_schema, char **stmt_schema_name)
 
 /*
  * transformPartitionCmd
 
@@ -4358,9 +4403,9 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 
@@ -4368,7 +4413,7 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
index 9f7c01b7fa..9c730e127c 100644
 
@@ -5134,7 +5141,7 @@ calculate_partition_bound_for_merge(Relation parent,
 													  (PartitionBoundSpec *) list_nth(bounds, prev_index),
 													  (RangeVar *) list_nth(partNames, index),
 													  (PartitionBoundSpec *) list_nth(bounds, index),
-													  pstate);
+													  false, pstate);
 				}
 
 				/*
@@ -5172,3 +5179,672 @@ calculate_partition_bound_for_merge(Relation parent,
 				 (int) key->strategy);
 	}
 }
+
+/*
 
 	return buf->data;
 }
+
+/*
 
 	/*

+/*

 
index 0000000000..d4519fbdbe
@@ -0,0 +1,1911 @@
+-- ERROR:  lower bound of partition "sales_mar2022" is not equal to the upper bound of partition "sales_feb2022"
+ERROR:  lower bound of partition "sales_mar2022" is not equal to the upper bound of partition "sales_feb2022"
+HINT:  ALTER TABLE ... MERGE PARTITIONS requires the partition bounds to be adjacent.
+-- ERROR:  lower bound of partition "sales_error" is not equal to the upper bound of partition "sales_dec2022"
+ERROR:  lower bound of partition "sales_error" is not equal to the upper bound of partition "sales_dec2022"
+HINT:  ALTER TABLE ... MERGE PARTITIONS requires the partition bounds to be adjacent.
+-- ERROR:  lower bound of partition "sales_feb2022" is not equal to the upper bound of partition "sales_error"
+ERROR:  lower bound of partition "sales_feb2022" is not equal to the upper bound of partition "sales_error"
+HINT:  ALTER TABLE ... MERGE PARTITIONS requires the partition bounds to be adjacent.
+-- ERROR:  lower bound of partition "sales_feb2022" is not equal to the upper bound of partition "sales_error"
+ERROR:  lower bound of partition "sales_error" is not equal to the upper bound of partition "sales_dec2022"
+HINT:  ALTER TABLE ... MERGE PARTITIONS requires the partition bounds to be adjacent.
+-- ERROR:  lower bound of partition "sales_feb2022" is not equal to the upper bound of partition "sales_error"
+ERROR:  lower bound of partition "sales_error" is not equal to the upper bound of partition "sales_dec2022"
+HINT:  ALTER TABLE ... MERGE PARTITIONS requires the partition bounds to be adjacent.
+HINT:  ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions
 
index 0000000000..b62d7dc30d
+-- ERROR:  lower bound of partition "sales_mar2022" is not equal to the upper bound of partition "sales_feb2022"
+-- ERROR:  lower bound of partition "sales_error" is not equal to the upper bound of partition "sales_dec2022"
+-- ERROR:  lower bound of partition "sales_feb2022" is not equal to the upper bound of partition "sales_error"
+-- ERROR:  lower bound of partition "sales_feb2022" is not equal to the upper bound of partition "sales_error"
+-- ERROR:  lower bound of partition "sales_feb2022" is not equal to the upper bound of partition "sales_error"
index 32d6e718ad..b815cced84 100644
@@ -2766,6 +2766,7 @@ SimpleStats
 SimpleStringList
 SimpleStringListCell
 SingleBoundSortItem
+SinglePartitionSpec
 Size
 SkipPages
 SkipSupport
@@ -2832,6 +2833,7 @@ SpecialJoinInfo
 SpinDelayStatus
 SplitInterval
 SplitLR
+SplitPartitionContext
 SplitPageLayout
 SplitPoint
 SplitTextOutputData
-- 
2.40.1.windows.1

---

From: jian he <[email-masked]>
Date: Tue, 17 Jun 2025 15:51:50 +0800

> Added patch and a bit modified test.

hi.
Please check the attached patch for addressing the following issues.

+ else
+ {
+ ereport(ERROR,
+ errcode(ERRCODE_DUPLICATE_TABLE),
+ errmsg("relation \"%s\" already exists", cmd->name->relname));
+ }
There are no regress tests for this, I have added a simple test for it.

SELECT c.oid::pg_catalog.regclass, c.relkind, inhdetachpending,
pg_catalog.pg_get_expr(c.relpartbound, c.oid)
  FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i
  WHERE c.oid = i.inhrelid AND i.inhparent = 'sales_range'::regclass
  ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT',
c.oid::pg_catalog.regclass::pg_catalog.text;

I found about 10 similar queries in partition_merge. We can replace them with
prepared statement—parsing once and executing multiple times. This not only
improves the readability of the test code but also slightly speeds up test
execution.

there are some test code pattern like:
+SELECT * FROM sales_range;
+SELECT * FROM sales_jan2022;
+SELECT * FROM partitions_merge_schema2.sales_feb_mar_apr2022;
+SELECT * FROM sales_others;
since sales_jan2022, sales_feb_mar_apr2022, sales_others
are partition of sales_range, we can simply use
+SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid,
salesperson_id;

After applying the attached patch, partition_merge.out size is 61379
without the size is: 66145.

The attached patch also addressed issues raised from Álvaro Herrera in [1].
instead of making function StoreConstraints external, using
AddRelationNewConstraints to install check constraints for new relation
in createTableConstraints

[1] https://postgr.es/m/[email-masked]

---

From: Dmitry Koval <[email-masked]>
Date: Wed, 18 Jun 2025 00:45:24 +0300

Hi!

 >v46-0001-regress-test-refactoring-and-others.no-cfbot

 >There are no regress tests for this, I have added a simple test for it.

Test for this is in the second commit (SPLIT PARTITION) but
it's okay to have test here.

 >+SELECT tableoid::regclass, * FROM sales_range ORDER BY ...

Similar changes are also made for SPLIT PARTITION.

-- 
With best regards,
Dmitry Koval

Postgres Professional: http://postgrespro.com
From a45ee53d9e475262a4834a53fb141613f34f3114 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:57:22 +0300
Subject: [PATCH v47 1/2] Implement ALTER TABLE ... MERGE PARTITIONS ...
 command

 src/backend/commands/tablecmds.c              |  912 ++++++++++++-
 src/backend/parser/parse_utilcmd.c            |  148 +++
 src/test/regress/expected/partition_merge.out | 1123 +++++++++++++++++
 src/test/regress/sql/partition_merge.sql      |  794 ++++++++++++
 21 files changed, 3632 insertions(+), 28 deletions(-)

 

   <para>
 
+  <para>
 
  <refsect1>
 
+/*
index fbaed5359a..54c95e4259 100644
@@ -102,11 +102,11 @@ static ObjectAddress AddNewRelationType(const char *typeName,
+static void StoreConstraints(Relation rel, List *cooked_constraints,
index ea96947d81..b724e2d715 100644

 
 
+/*

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -22035,3 +22072,844 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+
+/*
+	List	   *Constraints = NIL;
+		Constraint *constr;
+		constr = makeNode(Constraint);
+		constr->contype = CONSTR_CHECK;
+		constr->conname = pstrdup(ccname);
+		constr->deferrable = false;
+		constr->initdeferred = false;
+		constr->is_enforced = ccenforced;
+		constr->skip_validation = !ccvalid;
+		constr->initially_valid = ccvalid;
+		constr->is_no_inherit = ccnoinherit;
+		constr->raw_expr = NULL;
+		constr->cooked_expr = nodeToString(ccbin_node);
+		constr->location = -1;
+		Constraints = lappend(Constraints, constr);
+	/* install all CHECK constraints. */
+	cookedConstraints = AddRelationNewConstraints(newRel, NIL, Constraints,
+												  false, true, true, NULL);
+	/* Make the additional catalog changes visible */

 

+/*
+ * checkPartition

 				/*
 
 						/*

 

 
index 0000000000..f5e6ec6184
@@ -0,0 +1,1123 @@
+--ERROR, sales_apr_2 already exists
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_jan2022) INTO sales_apr_2;
+ERROR:  relation "sales_apr_2" already exists
+PREPARE get_partition_info(regclass[]) AS
+SELECT  c.oid::pg_catalog.regclass,
+        c.relpersistence,
+        c.relkind,
+        i.inhdetachpending,
+        pg_catalog.pg_get_expr(c.relpartbound, c.oid)
+FROM    pg_catalog.pg_class c, pg_catalog.pg_inherits i
+WHERE   c.oid = i.inhrelid AND i.inhparent = ANY($1)
+ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT',
+         c.oid::regclass::text COLLATE "C";
+EXECUTE get_partition_info('{sales_range}');
+                      oid                       | relpersistence | relkind | inhdetachpending |                   pg_get_expr                    
+------------------------------------------------+----------------+---------+------------------+--------------------------------------------------
+ partitions_merge_schema2.sales_jan_feb_mar2022 | p              | r       | f                | FOR VALUES FROM ('01-01-2022') TO ('04-01-2022')
+ sales_apr2022                                  | p              | p       | f                | FOR VALUES FROM ('04-01-2022') TO ('05-01-2022')
+ sales_dec2021                                  | p              | r       | f                | FOR VALUES FROM ('12-01-2021') TO ('12-31-2021')
+ sales_others                                   | p              | r       | f                | DEFAULT
+EXECUTE get_partition_info('{sales_range}');
+      oid      | relpersistence | relkind | inhdetachpending |                   pg_get_expr                    
+---------------+----------------+---------+------------------+--------------------------------------------------
+ sales_apr2022 | p              | r       | f                | FOR VALUES FROM ('04-01-2022') TO ('05-01-2022')
+ sales_feb2022 | p              | r       | f                | FOR VALUES FROM ('02-01-2022') TO ('03-01-2022')
+ sales_jan2022 | p              | r       | f                | FOR VALUES FROM ('01-01-2022') TO ('02-01-2022')
+ sales_mar2022 | p              | r       | f                | FOR VALUES FROM ('03-01-2022') TO ('04-01-2022')
+ sales_others  | p              | r       | f                | DEFAULT
+EXECUTE get_partition_info('{sales_range}');
+                      oid                       | relpersistence | relkind | inhdetachpending |                   pg_get_expr                    
+------------------------------------------------+----------------+---------+------------------+--------------------------------------------------
+ partitions_merge_schema2.sales_feb_mar_apr2022 | p              | r       | f                | FOR VALUES FROM ('02-01-2022') TO ('05-01-2022')
+ sales_jan2022                                  | p              | r       | f                | FOR VALUES FROM ('01-01-2022') TO ('02-01-2022')
+ sales_others                                   | p              | r       | f                | DEFAULT
+SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid, salesperson_id;
+                    tableoid                    | salesperson_id | salesperson_name | sales_amount | sales_date 
+------------------------------------------------+----------------+------------------+--------------+------------
+ sales_jan2022                                  |              1 | May              |         1000 | 01-31-2022
+ sales_jan2022                                  |             10 | Halder           |          350 | 01-28-2022
+ sales_jan2022                                  |             13 | Gandi            |          377 | 01-09-2022
+ sales_others                                   |             14 | Smith            |          510 | 05-04-2022
+ partitions_merge_schema2.sales_feb_mar_apr2022 |              2 | Smirnoff         |          500 | 02-10-2022
+ partitions_merge_schema2.sales_feb_mar_apr2022 |              3 | Ford             |         2000 | 04-30-2022
+ partitions_merge_schema2.sales_feb_mar_apr2022 |              4 | Ivanov           |          750 | 04-13-2022
+ partitions_merge_schema2.sales_feb_mar_apr2022 |              5 | Deev             |          250 | 04-07-2022
+ partitions_merge_schema2.sales_feb_mar_apr2022 |              6 | Poirot           |          150 | 02-11-2022
+ partitions_merge_schema2.sales_feb_mar_apr2022 |              7 | Li               |          175 | 03-08-2022
+ partitions_merge_schema2.sales_feb_mar_apr2022 |              8 | Ericsson         |          185 | 02-23-2022
+ partitions_merge_schema2.sales_feb_mar_apr2022 |              9 | Muller           |          250 | 03-11-2022
+ partitions_merge_schema2.sales_feb_mar_apr2022 |             11 | Trump            |          380 | 04-06-2022
+ partitions_merge_schema2.sales_feb_mar_apr2022 |             12 | Plato            |          350 | 03-19-2022
+SELECT * FROM sales_others ORDER BY salesperson_id;
+EXECUTE get_partition_info('{sales_range}');
+      oid      | relpersistence | relkind | inhdetachpending |                   pg_get_expr                    
+---------------+----------------+---------+------------------+--------------------------------------------------
+ sales_apr2022 | p              | r       | f                | FOR VALUES FROM ('04-01-2022') TO ('05-01-2022')
+ sales_feb2022 | p              | r       | f                | FOR VALUES FROM ('02-01-2022') TO ('03-01-2022')
+ sales_others  | p              | r       | f                | DEFAULT
+SELECT tableoid::regclass, * FROM sales_apr2022 ORDER BY tableoid, salesperson_id;
+      tableoid       | salesperson_id | salesperson_name | sales_amount | sales_date 
+---------------------+----------------+------------------+--------------+------------
+ sales_apr2022_01_10 |              5 | Deev             |          250 | 04-07-2022
+ sales_apr2022_01_10 |             11 | Trump            |          380 | 04-06-2022
+ sales_apr2022_10_20 |              4 | Ivanov           |          750 | 04-13-2022
+ sales_apr2022_20_30 |              3 | Ford             |         2000 | 04-30-2022
+SELECT tableoid::regclass, * FROM sales_apr2022 ORDER BY tableoid, salesperson_id;
+   tableoid    | salesperson_id | salesperson_name | sales_amount | sales_date 
+---------------+----------------+------------------+--------------+------------
+ sales_apr_all |              3 | Ford             |         2000 | 04-30-2022
+ sales_apr_all |              4 | Ivanov           |          750 | 04-13-2022
+ sales_apr_all |              5 | Deev             |          250 | 04-07-2022
+ sales_apr_all |             11 | Trump            |          380 | 04-06-2022
+EXECUTE get_partition_info('{sales_list}');
+      oid      | relpersistence | relkind | inhdetachpending |                     pg_get_expr                      
+---------------+----------------+---------+------------------+------------------------------------------------------
+ sales_central | p              | r       | f                | FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')
+ sales_east    | p              | r       | f                | FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok')
+ sales_nord    | p              | r       | f                | FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki')
+ sales_west    | p              | r       | f                | FOR VALUES IN ('Lisbon', 'New York', 'Madrid')
+ sales_others  | p              | r       | f                | DEFAULT
+EXECUTE get_partition_info('{sales_list}');
+     oid      | relpersistence | relkind | inhdetachpending |                                                 pg_get_expr                                                  
+--------------+----------------+---------+------------------+--------------------------------------------------------------------------------------------------------------
+ sales_all    | p              | r       | f                | FOR VALUES IN ('Lisbon', 'New York', 'Madrid', 'Bejing', 'Delhi', 'Vladivostok', 'Warsaw', 'Berlin', 'Kyiv')
+ sales_nord   | p              | r       | f                | FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki')
+ sales_others | p              | r       | f                | DEFAULT
+EXECUTE get_partition_info('{t}');
+  oid   | relpersistence | relkind | inhdetachpending |        pg_get_expr         
+--------+----------------+---------+------------------+----------------------------
+ tp_0_1 | t              | r       | f                | FOR VALUES FROM (0) TO (1)
+ tp_1_2 | t              | r       | f                | FOR VALUES FROM (1) TO (2)
+EXECUTE get_partition_info('{t}');
+  oid   | relpersistence | relkind | inhdetachpending |        pg_get_expr         
+--------+----------------+---------+------------------+----------------------------
+ tp_0_2 | t              | r       | f                | FOR VALUES FROM (0) TO (2)
+EXECUTE get_partition_info('{t}');
+  oid   | relpersistence | relkind | inhdetachpending |        pg_get_expr         
+--------+----------------+---------+------------------+----------------------------
+ tp_0_1 | p              | r       | f                | FOR VALUES FROM (0) TO (1)
+ tp_1_2 | p              | r       | f                | FOR VALUES FROM (1) TO (2)
+EXECUTE get_partition_info('{t}');
+  oid   | relpersistence | relkind | inhdetachpending |        pg_get_expr         
+--------+----------------+---------+------------------+----------------------------
+ tp_0_1 | t              | r       | f                | FOR VALUES FROM (0) TO (1)
+ tp_1_2 | t              | r       | f                | FOR VALUES FROM (1) TO (2)
+DEALLOCATE get_partition_info;
 
index 0000000000..2cdd58b200
@@ -0,0 +1,794 @@
+--ERROR, sales_apr_2 already exists
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_jan2022) INTO sales_apr_2;
+PREPARE get_partition_info(regclass[]) AS
+SELECT  c.oid::pg_catalog.regclass,
+        c.relpersistence,
+        c.relkind,
+        i.inhdetachpending,
+        pg_catalog.pg_get_expr(c.relpartbound, c.oid)
+FROM    pg_catalog.pg_class c, pg_catalog.pg_inherits i
+WHERE   c.oid = i.inhrelid AND i.inhparent = ANY($1)
+ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT',
+         c.oid::regclass::text COLLATE "C";
+EXECUTE get_partition_info('{sales_range}');
+EXECUTE get_partition_info('{sales_range}');
+EXECUTE get_partition_info('{sales_range}');
+SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid, salesperson_id;
+SELECT * FROM sales_others ORDER BY salesperson_id;
+EXECUTE get_partition_info('{sales_range}');
+SELECT tableoid::regclass, * FROM sales_apr2022 ORDER BY tableoid, salesperson_id;
+SELECT tableoid::regclass, * FROM sales_apr2022 ORDER BY tableoid, salesperson_id;
+EXECUTE get_partition_info('{sales_list}');
+EXECUTE get_partition_info('{sales_list}');
+EXECUTE get_partition_info('{t}');
+EXECUTE get_partition_info('{t}');
+EXECUTE get_partition_info('{t}');
+EXECUTE get_partition_info('{t}');
+DEALLOCATE get_partition_info;

From dd7741235b60ae99144a8b699e875cd3c1dface2 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:58:09 +0300
Subject: [PATCH v47 2/2] Implement ALTER TABLE ... SPLIT PARTITION ... command

 src/backend/commands/tablecmds.c              |  463 +++++
 src/backend/partitioning/partbounds.c         |  684 ++++++-
 src/test/regress/expected/partition_split.out | 1655 +++++++++++++++++
 src/test/regress/sql/partition_split.sql      | 1148 ++++++++++++
 20 files changed, 4444 insertions(+), 23 deletions(-)

 

+  <para>
index b724e2d715..70ddf7a050 100644

 
@@ -22913,3 +22935,444 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,

 
+partitions_list:
 
+/*

 
 /*
  * transformPartitionCmd

 
 
 				/*
 
 	return buf->data;
 }
+
+/*
 
 	/*

+/*

 
index 0000000000..7d543ab813
@@ -0,0 +1,1655 @@
+CREATE TABLE sales_dec2021 PARTITION OF sales_date FOR VALUES FROM (2021, 12, 1) TO (2022, 1, 1);
+SELECT tableoid::regclass, * FROM sales_date ORDER BY tableoid, sales_year, sales_month, sales_day;
+     tableoid      | salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department 
+-------------------+------------------+------------+-------------+-----------+------------+------------------
+ sales_dec2021     | Manager1         |       2021 |          12 |         7 | 2021.12.07 | Sales department
+ sales_dec2021     | Manager2         |       2021 |          12 |         8 | 2021.12.08 | Sales department
+ sales_jan_feb2022 | Manager3         |       2022 |           1 |         1 | 2022.01.01 | Sales department
+ sales_jan_feb2022 | Manager2         |       2022 |           1 |         2 | 2022.01.02 | Sales department
+ sales_jan_feb2022 | Manager3         |       2022 |           2 |         1 | 2022.02.01 | Sales department
+ sales_jan_feb2022 | Manager1         |       2022 |           2 |         4 | 2022.02.04 | Sales department
+ sales_other       | Manager1         |       2022 |           3 |         3 | 2022.03.03 | Sales department
+ sales_other       | Manager2         |       2022 |           3 |         4 | 2022.03.04 | Sales department
+ sales_other       | Manager3         |       2022 |           5 |         1 | 2022.05.01 | Sales department
+SELECT tableoid::regclass, * FROM sales_date ORDER BY tableoid, sales_year, sales_month, sales_day;
+   tableoid    | salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department 
+---------------+------------------+------------+-------------+-----------+------------+------------------
+ sales_dec2021 | Manager1         |       2021 |          12 |         7 | 2021.12.07 | Sales department
+ sales_dec2021 | Manager2         |       2021 |          12 |         8 | 2021.12.08 | Sales department
+ sales_other   | Manager1         |       2022 |           3 |         3 | 2022.03.03 | Sales department
+ sales_other   | Manager2         |       2022 |           3 |         4 | 2022.03.04 | Sales department
+ sales_other   | Manager3         |       2022 |           5 |         1 | 2022.05.01 | Sales department
+ sales_jan2022 | Manager3         |       2022 |           1 |         1 | 2022.01.01 | Sales department
+ sales_jan2022 | Manager2         |       2022 |           1 |         2 | 2022.01.02 | Sales department
+ sales_jan2022 | Manager1         |       2022 |           1 |        10 | 2022.01.10 | Sales department
+ sales_feb2022 | Manager3         |       2022 |           2 |         1 | 2022.02.01 | Sales department
+ sales_feb2022 | Manager1         |       2022 |           2 |         4 | 2022.02.04 | Sales department
+ sales_feb2022 | Manager2         |       2022 |           2 |        10 | 2022.02.10 | Sales department
+-- sales_error intersects with sales_dec2021 (lower bound)
+-- ERROR:  lower bound of partition "sales_error" is not equal to the upper bound of partition "sales_dec2021"
+  (PARTITION sales_dec2021 FOR VALUES FROM (20211201) TO (20220101),
+ERROR:  lower bound of partition "sales_error" is not equal to the upper bound of partition "sales_dec2021"
+  (PARTITION sales_dec2021 FOR VALUES FROM (20211201) TO (20220101),
+-- sales_error intersects with sales_dec2021 (inside bound)
+  (PARTITION sales_dec2021 FOR VALUES FROM (20211201) TO (20220101),
+ERROR:  lower bound of partition "sales_error" is not equal to the upper bound of partition "sales_dec2021"
+-- sales_error intersects with sales_dec2021 (exactly the same bounds)
+  (PARTITION sales_dec2021 FOR VALUES FROM (20211201) TO (20220101),
+ERROR:  lower bound of partition "sales_error" is not equal to the upper bound of partition "sales_dec2021"
+  (PARTITION sales_dec2021 FOR VALUES FROM (20211201) TO (20220101),
+LINE 2:   (PARTITION sales_dec2021 FOR VALUES FROM (20211201) TO (20...
+-- no error: bounds of sales_noerror are between sales_dec2021 and sales_feb2022
+  (PARTITION sales_dec2021 FOR VALUES FROM (20211201) TO (20220101),
+-- no error: bounds of sales_noerror are equal to lower and upper bounds of sales_dec2021 and sales_feb2022
+  (PARTITION sales_dec2021 FOR VALUES FROM (20211201) TO (20220101),
+SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid, salesperson_id;
+     tableoid     | salesperson_id | salesperson_name 
+------------------+----------------+------------------
+ salespeople01_10 |              1 | Poirot
+ salespeople10_40 |             10 | May
+ salespeople10_40 |             19 | Ivanov
+ salespeople10_40 |             20 | Smirnoff
+ salespeople10_40 |             30 | Ford
+SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid, salesperson_id;
+     tableoid     | salesperson_id | salesperson_name 
+------------------+----------------+------------------
+ salespeople01_10 |              1 | Poirot
+ salespeople10_20 |             10 | May
+ salespeople10_20 |             19 | Ivanov
+ salespeople20_30 |             20 | Smirnoff
+ salespeople30_40 |             30 | Ford
+SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid, salesperson_id;
+     tableoid     | salesperson_id | salesperson_name 
+------------------+----------------+------------------
+ salespeople01_10 |              1 | Poirot
+ salespeople10_20 |             10 | May
+ salespeople10_20 |             19 | Ivanov
+ salespeople20_30 |             20 | Smirnoff
+ salespeople30_40 |             30 | Ford
+SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid, salesperson_id;
+    tableoid    | salesperson_id | salesperson_name 
+----------------+----------------+------------------
+ salespeople1_2 |              1 | Poirot
+ salespeople2_3 |              2 | Ivanov
+ salespeople3_4 |              3 | May
+ salespeople4_5 |              4 | Ford
+SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid, salesperson_id;
+     tableoid     | salesperson_id | salesperson_name 
+------------------+----------------+------------------
+ salespeople01_10 |              1 | Poirot
+ salespeople10_20 |             10 | May
+ salespeople10_20 |             19 | Ivanov
+ salespeople20_30 |             20 | Smirnoff
+ salespeople30_40 |             30 | Ford
+SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid, salesperson_id;
+      tableoid       | salesperson_id | salesperson_name | sales_amount | sales_date 
+---------------------+----------------+------------------+--------------+------------
+ sales_jan2022       |              1 | May              |         1000 | 01-31-2022
+ sales_jan2022       |             10 | Halder           |          350 | 01-28-2022
+ sales_jan2022       |             13 | Gandi            |          377 | 01-09-2022
+ sales_feb2022       |              2 | Smirnoff         |          500 | 02-10-2022
+ sales_feb2022       |              6 | Poirot           |          150 | 02-11-2022
+ sales_feb2022       |              8 | Ericsson         |          185 | 02-23-2022
+ sales_mar2022       |              7 | Li               |          175 | 03-08-2022
+ sales_mar2022       |              9 | Muller           |          250 | 03-11-2022
+ sales_mar2022       |             12 | Plato            |          350 | 03-19-2022
+ sales_others        |             14 | Smith            |          510 | 05-04-2022
+ sales_apr2022_01_10 |              5 | Deev             |          250 | 04-07-2022
+ sales_apr2022_01_10 |             11 | Trump            |          380 | 04-06-2022
+ sales_apr2022_10_20 |              4 | Ivanov           |          750 | 04-13-2022
+ sales_apr2022_20_30 |              3 | Ford             |         2000 | 04-30-2022
+SELECT tableoid::regclass, * FROM sales_list ORDER BY tableoid, salesperson_id;
+   tableoid    | salesperson_id | salesperson_name |  sales_state   | sales_amount | sales_date 
+---------------+----------------+------------------+----------------+--------------+------------
+ sales_nord    |              3 | Ford             | St. Petersburg |         2000 | 03-05-2022
+ sales_nord    |              7 | May              | Oslo           |         1200 | 03-06-2022
+ sales_nord    |              9 | May              | Oslo           |         1200 | 03-11-2022
+ sales_nord    |             10 | Halder           | Helsinki       |          800 | 03-02-2022
+ sales_west    |              2 | Smirnoff         | New York       |          500 | 03-03-2022
+ sales_west    |              5 | Deev             | Lisbon         |          250 | 03-07-2022
+ sales_west    |             11 | Muller           | Madrid         |          650 | 03-05-2022
+ sales_west    |             14 | Plato            | Lisbon         |          950 | 03-05-2022
+ sales_east    |              1 | Trump            | Bejing         |         1000 | 03-01-2022
+ sales_east    |              8 | Li               | Vladivostok    |         1150 | 03-09-2022
+ sales_central |              4 | Ivanov           | Warsaw         |          750 | 03-04-2022
+ sales_central |              6 | Poirot           | Berlin         |         1000 | 03-01-2022
+ sales_central |             12 | Smith            | Kyiv           |          350 | 03-10-2022
+ sales_central |             13 | Gandi            | Warsaw         |          150 | 03-08-2022
+SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid, salesperson_id;
+       tableoid        | salesperson_id | salesperson_name | sales_amount | sales_date 
+-----------------------+----------------+------------------+--------------+------------
+ sales_others          |              1 | May              |         1000 | 01-31-2022
+ sales_others          |              3 | Ford             |         2000 | 04-30-2022
+ sales_others          |              4 | Ivanov           |          750 | 04-13-2022
+ sales_others          |              8 | Ericsson         |          185 | 02-23-2022
+ sales_others          |              9 | Muller           |          250 | 03-11-2022
+ sales_others          |             10 | Halder           |          350 | 01-28-2022
+ sales_others          |             12 | Plato            |          350 | 03-19-2022
+ sales_others          |             14 | Smith            |          510 | 05-04-2022
+ sales_mar2022_1decade |              7 | Li               |          175 | 03-08-2022
+ sales_jan2022_1decade |             13 | Gandi            |          377 | 01-09-2022
+ sales_feb2022_1decade |              2 | Smirnoff         |          500 | 02-09-2022
+ sales_feb2022_1decade |              6 | Poirot           |          150 | 02-07-2022
+ sales_apr2022_1decade |              5 | Deev             |          250 | 04-07-2022
+ sales_apr2022_1decade |             11 | Trump            |          380 | 04-06-2022
+SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid, salesperson_id;
+       tableoid        | salesperson_id | salesperson_name | sales_amount | sales_date 
+-----------------------+----------------+------------------+--------------+------------
+ sales_others          |              1 | May              |         1000 | 01-31-2022
+ sales_others          |              3 | Ford             |         2000 | 04-30-2022
+ sales_others          |              4 | Ivanov           |          750 | 04-13-2022
+ sales_others          |              8 | Ericsson         |          185 | 02-23-2022
+ sales_others          |              9 | Muller           |          250 | 03-11-2022
+ sales_others          |             10 | Halder           |          350 | 01-28-2022
+ sales_others          |             12 | Plato            |          350 | 03-19-2022
+ sales_others          |             14 | Smith            |          510 | 05-04-2022
+ sales_mar2022_1decade |              7 | Li               |          175 | 03-08-2022
+ sales_jan2022_1decade |             13 | Gandi            |          377 | 01-09-2022
+ sales_feb2022_1decade |              2 | Smirnoff         |          500 | 02-09-2022
+ sales_feb2022_1decade |              6 | Poirot           |          150 | 02-07-2022
+ sales_apr2022_1decade |              5 | Deev             |          250 | 04-07-2022
+ sales_apr2022_1decade |             11 | Trump            |          380 | 04-06-2022
+SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid, salesperson_id;
+   tableoid    | salesperson_id | salesperson_name | sales_amount | sales_date 
+---------------+----------------+------------------+--------------+------------
+ sales_jan2022 |              1 | May              |         1000 | 01-31-2022
+ sales_jan2022 |             10 | Halder           |          350 | 01-28-2022
+ sales_jan2022 |             13 | Gandi            |          377 | 01-09-2022
+ sales_apr2022 |              3 | Ford             |         2000 | 04-30-2022
+ sales_apr2022 |              4 | Ivanov           |          750 | 04-13-2022
+ sales_apr2022 |              5 | Deev             |          250 | 04-07-2022
+ sales_apr2022 |             11 | Trump            |          380 | 04-06-2022
+ sales_feb2022 |              2 | Smirnoff         |          500 | 02-10-2022
+ sales_feb2022 |              6 | Poirot           |          150 | 02-11-2022
+ sales_feb2022 |              8 | Ericsson         |          185 | 02-23-2022
+ sales_others  |              7 | Li               |          175 | 03-08-2022
+ sales_others  |              9 | Muller           |          250 | 03-11-2022
+ sales_others  |             12 | Plato            |          350 | 03-19-2022
+ sales_others  |             14 | Smith            |          510 | 05-04-2022
 
index 0000000000..7fa4b69376
@@ -0,0 +1,1148 @@
+CREATE TABLE sales_dec2021 PARTITION OF sales_date FOR VALUES FROM (2021, 12, 1) TO (2022, 1, 1);
+SELECT tableoid::regclass, * FROM sales_date ORDER BY tableoid, sales_year, sales_month, sales_day;
+SELECT tableoid::regclass, * FROM sales_date ORDER BY tableoid, sales_year, sales_month, sales_day;
+-- sales_error intersects with sales_dec2021 (lower bound)
+-- ERROR:  lower bound of partition "sales_error" is not equal to the upper bound of partition "sales_dec2021"
+  (PARTITION sales_dec2021 FOR VALUES FROM (20211201) TO (20220101),
+  (PARTITION sales_dec2021 FOR VALUES FROM (20211201) TO (20220101),
+-- sales_error intersects with sales_dec2021 (inside bound)
+  (PARTITION sales_dec2021 FOR VALUES FROM (20211201) TO (20220101),
+-- sales_error intersects with sales_dec2021 (exactly the same bounds)
+  (PARTITION sales_dec2021 FOR VALUES FROM (20211201) TO (20220101),
+  (PARTITION sales_dec2021 FOR VALUES FROM (20211201) TO (20220101),
+-- no error: bounds of sales_noerror are between sales_dec2021 and sales_feb2022
+  (PARTITION sales_dec2021 FOR VALUES FROM (20211201) TO (20220101),
+-- no error: bounds of sales_noerror are equal to lower and upper bounds of sales_dec2021 and sales_feb2022
+  (PARTITION sales_dec2021 FOR VALUES FROM (20211201) TO (20220101),
+SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid, salesperson_id;
+SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid, salesperson_id;
+SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid, salesperson_id;
+SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid, salesperson_id;
+SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid, salesperson_id;
+SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid, salesperson_id;
+SELECT tableoid::regclass, * FROM sales_list ORDER BY tableoid, salesperson_id;
+SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid, salesperson_id;
+SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid, salesperson_id;
+SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid, salesperson_id;

---

From: jian he <[email-masked]>
Date: Wed, 18 Jun 2025 10:55:38 +0800

hi.

The following are changes I made based on v47.
mainly comments refactoring, variable/argument renaming.
please see the attached patch.

+
+ /* Create the relation. */
+ newRelId = heap_create_with_catalog(newPartName->relname,
+ namespaceId,
+ modelRel->rd_rel->reltablespace,
+....
+ allowSystemTableMods,
+ false,
+ InvalidOid,
+ NULL);
heap_create_with_catalog parameter is_internal should be set to true.

+ /*
+ * Construct a map from the LIKE relation's attnos to the child rel's.
+ * This re-checks type match etc, although it shouldn't be possible to
+ * have a failure since both tables are locked.
+ */
+ attmap = build_attrmap_by_name(RelationGetDescr(newRel),
+   tupleDesc,
+   false);
this comment in createTableConstraints is confusing, especially the word "LIKE".
I didn' change it though.

+/*
+ * moveMergedTablesRows: scan partitions to be merged (mergingPartitions)
+ * of the partitioned table (rel) and move rows into the new partition
+ * (newPartRel). We also reevaulate check constraints against these rows.
+ */
+static void
+moveMergedTablesRows(List **wqueue, Relation rel,
+ List *mergingPartitions, Relation newPartRel)
the argument (Relation rel) never used in moveMergedTablesRows
we can remove it, or rename it as "parent_rel".
I didn' change it though.

moveMergedTablesRows was never used in SPLIT PARTITION,
so maybe we can rename it to
ATMergePartitionMoveTablesRows
or
ATMergePartitionMoveRows
or
ATMergePartitionRows
what do you think?

check_two_partitions_bounds_range
we can name it to
range_partition_bounds_check
I don't have a huge opinion though.

createTableConstraints(List **wqueue, AlteredTableInfo *tab,
                       Relation modelRel, Relation newRel)
rename argument (Relation modelRel) to parent_rel,
I think it will improve readability, since "parent_rel" can tell you it's a
partitioned table.

ATExecMergePartitions some comments position adjusted.

minor change src/test/regress/sql/partition_merge.sql
-SELECT * FROM sales_list;
-SELECT * FROM sales_nord;
-SELECT * FROM sales_all;
+SELECT tableoid::regclass, * FROM sales_list ORDER BY tableoid, salesperson_id;

we need an error case for
ERROR:  cannot drop table \"%s\" because other objects depend on it
so I added a test for it. as you can see below, the error HINT message
is not great in this
context.

CREATE VIEW jan2022v as SELECT * FROM sales_jan2022;
ALTER TABLE sales_range MERGE PARTITIONS (sales_jan2022,
sales_feb2022) INTO sales_dec_jan_feb2022;
ERROR:  cannot drop table sales_jan2022 because other objects depend on it
DETAIL:  view jan2022v depends on table sales_jan2022
HINT:  Use DROP ... CASCADE to drop the dependent objects too.
DROP VIEW jan2022v;

---

From: Dmitry Koval <[email-masked]>
Date: Thu, 19 Jun 2025 02:12:38 +0300

Hi!

1.
 >v47-0001-rename-function-argument-and-minor-refactor.no-cfbot

Thanks, applied.

2.
 >+ * Construct a map from the LIKE relation's attnos to the child rel's
 >this comment in createTableConstraints is confusing, especially the
 > word "LIKE". I didn' change it though.

It is copy from expandTableLikeClause function. Changed.

3.
 >I didn' change it though.

Removed.

4.
 >ATMergePartitionMoveTablesRows

I like the name "MergePartitionsMoveRows" (without prefix "AT" - "ALTER 
TABLE", because this function is not called from ATExecCmd function).
Is it ok?

5.
 >is not great in this context.
...
 >HINT:  Use DROP ... CASCADE to drop the dependent objects too.

Maybe a special flag (DEPFLAG_NOHINT?) should be added to skip hints for 
the performDeletionCheck function?

-- 
With best regards,
Dmitry Koval

From a52039cdba5fd788747cfacf6037a103d5424881 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:57:22 +0300
Subject: [PATCH v48 1/2] Implement ALTER TABLE ... MERGE PARTITIONS ...
 command

 src/backend/commands/tablecmds.c              |  908 +++++++++++++-
 src/test/regress/expected/partition_merge.out | 1105 +++++++++++++++++
 src/test/regress/sql/partition_merge.sql      |  796 ++++++++++++
 21 files changed, 3612 insertions(+), 28 deletions(-)

 

   <para>
 
+  <para>
 
  <refsect1>
 
+/*
index 10f43c51c5..696f80be83 100644
index ea96947d81..7f63df0612 100644

 
 
+/*

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -22035,3 +22072,840 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+
+/*
+ * getAttributesList: build a list of columns (ColumnDef) based on parent_rel
+getAttributesList(Relation parent_rel)
+	modelDesc = RelationGetDescr(parent_rel);
+ * createTableConstraints:
+ * create check constraints, default values and generated values for newRel
+ * based on parent_rel.  tab is pending-work queue for newRel, we may need it in
+ * MergePartitionsMoveRows.
+					   Relation parent_rel, Relation newRel)
+	tupleDesc = RelationGetDescr(parent_rel);
+	 * Construct a map from the parent relation's attnos to the child rel's.
+				this_default = build_generation_expression(parent_rel, attribute->attnum);
+						 attribute->attnum, RelationGetRelationName(parent_rel));
+								 RelationGetRelationName(parent_rel)));
+			 * Stored generated column expressions in parent_rel might reference
+			 * tableoid.  newRel, parent_rel tableoid clear is not the same. If
+			 * newRel within MergePartitionsMoveRows.
+							   RelationGetRelationName(parent_rel)));
+	 * parent_rel check constraint expresssion may reference tableoid, so later
+	 * in MergePartitionsMoveRows, we need evulate the check constraint again
+	 * for the newRel. We can check weather check constraint contain tableoid
+	 * reference or not via pull_varattnos.
+			 * Add check only if it contains tableoid (TableOidAttributeNumber).
+		nnconstraints = RelationGetNotNullConstraints(RelationGetRelid(parent_rel),
+ * createPartitionTable:
+ * Create a new partition (newPartName) for partitioned table (parent_rel).
+ * ownerId is determined by the partition on which the operation is performed,
+ * so it is passed separately.  The new partition will inherit the access method
+ * and persistence type from the parent table.
+ * returns the created relation (locked in AccessExclusiveLock mode).
+					 Relation parent_rel, Oid ownerId)
+	if (RELATION_IS_OTHER_TEMP(parent_rel))
+	colList = getAttributesList(parent_rel);
+	relamId = (parent_rel->rd_rel->relam != InvalidOid) ? parent_rel->rd_rel->relam : HEAP_TABLE_AM_OID;
+										parent_rel->rd_rel->reltablespace,
+	if (parent_rel->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
+					   RelationGetRelationName(parent_rel)));
+		parent_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+					   RelationGetRelationName(parent_rel)));
+	createTableConstraints(wqueue, new_partrel_tab, parent_rel, newRel);
+ * MergePartitionsMoveRows: scan partitions to be merged (mergingPartitions)
+ * of the partitioned table and move rows into the new partition
+MergePartitionsMoveRows(List **wqueue, List *mergingPartitions, Relation newPartRel)
+	/* Detach all merged partitions */
+	 * into the new partitions via MergePartitionsMoveRows, all old partitions
+	 * need be dropped. However, since the drop behavior is DROP_RESTRICT and
+	 * the merge process (MergePartitionsMoveRows) can be time-consuming,
+	 * performing an early check on the drop eligibility of old partitions is
+	 * preferable.
+	MergePartitionsMoveRows(wqueue, mergingPartitions, newPartRel);

 

+/*
+ * checkPartition

 				/*
 
 						/*

 

 
index 0000000000..531f2021aa
@@ -0,0 +1,1105 @@
+CREATE VIEW jan2022v as SELECT * FROM sales_jan2022;
+ALTER TABLE sales_range MERGE PARTITIONS (sales_jan2022, sales_feb2022) INTO sales_dec_jan_feb2022;
+ERROR:  cannot drop table sales_jan2022 because other objects depend on it
+DETAIL:  view jan2022v depends on table sales_jan2022
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+DROP VIEW jan2022v;
+------------+----------------+------------------+----------------+--------------+------------
+ sales_nord |              7 | May              | Helsinki       |         1200 | 03-06-2022
+ sales_nord |              9 | May              | Helsinki       |         1200 | 03-11-2022
+ sales_nord |             10 | Halder           | Oslo           |          800 | 03-02-2022
+ sales_all  |              1 | Trump            | Bejing         |         1000 | 03-01-2022
+ sales_all  |              2 | Smirnoff         | New York       |          500 | 03-03-2022
+ sales_all  |              4 | Ivanov           | Warsaw         |          750 | 03-04-2022
+ sales_all  |              5 | Deev             | Lisbon         |          250 | 03-07-2022
+ sales_all  |              6 | Poirot           | Berlin         |         1000 | 03-01-2022
+ sales_all  |              8 | Li               | Vladivostok    |         1150 | 03-09-2022
+ sales_all  |             11 | Muller           | Madrid         |          650 | 03-05-2022
+ sales_all  |             12 | Smith            | Kyiv           |          350 | 03-10-2022
+ sales_all  |             13 | Gandi            | Warsaw         |          150 | 03-08-2022
+ sales_all  |             14 | Plato            | Lisbon         |          950 | 03-05-2022
 
index 0000000000..bf8acc5136
@@ -0,0 +1,796 @@
+CREATE VIEW jan2022v as SELECT * FROM sales_jan2022;
+ALTER TABLE sales_range MERGE PARTITIONS (sales_jan2022, sales_feb2022) INTO sales_dec_jan_feb2022;
+DROP VIEW jan2022v;

From 7f8efea72095bfab3bc8bd039c6e345b22495a87 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:58:09 +0300
Subject: [PATCH v48 2/2] Implement ALTER TABLE ... SPLIT PARTITION ... command

 src/backend/commands/tablecmds.c              |  465 +++++
 20 files changed, 4446 insertions(+), 23 deletions(-)

 

+  <para>
index 7f63df0612..d3edfce184 100644

 
@@ -22909,3 +22931,446 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
+ * SplitPartitionMoveRows: scan split partition (splitRel) of partitioned table
+SplitPartitionMoveRows(List **wqueue, Relation rel, Relation splitRel,
+					   List *partlist, List *newPartRels, Oid defaultPartOid)
+	 * into the new partitions via SplitPartitionMoveRows, all old partitions
+	 * need be dropped. However, since the drop behavior is DROP_RESTRICT and
+	 * the merge process (SplitPartitionMoveRows) can be time-consuming,
+	 * performing an early check on the drop eligibility of old partitions is
+	 * preferable.
+	SplitPartitionMoveRows(wqueue, rel, splitRel, cmd->partlist, newPartRels, defaultPartOid);

 
+partitions_list:
 
+/*

 
 /*
  * transformPartitionCmd

 
 
 				/*
 
 	return buf->data;
 }
+
+/*
 
 	/*

+/*

---

From: jian he <[email-masked]>
Date: Tue, 24 Jun 2025 16:06:56 +0800

hi.

+ /* Sort array of lower bounds. */
+ qsort_arg(lower_bounds, nparts, sizeof(PartitionRangeBound *),
+  qsort_partition_rbound_cmp, (void *) key);
here, we don't need ``(void *)``

+ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
+    MERGE PARTITIONS (<replaceable
class="parameter">partition_name1</replaceable>, <replaceable
class="parameter">partition_name2</replaceable> [, ...])
+        INTO <replaceable class="parameter">partition_name</replaceable>
In the synopsis section, we can combine the last two lines into one
for better formatting.

after
<varlistentry id="sql-altertable-parms-partition-name">
we can add the following to briefly explain parameters: partition_name1,
partition_name2

     <varlistentry id="sql-altertable-parms-partition-name1">
      <term><replaceable class="parameter">partition_name1</replaceable></term>
      <term><replaceable class="parameter">partition_name2</replaceable></term>
      <listitem>
       <para>
        The names of the tables being merged into the new partition.
       </para>
      </listitem>
     </varlistentry>

What do you think about alternative syntax:
ALTER TABLE tab1 MERGE PARTITION part1 WITH (part2, part3) mentioned in [1].
I think we need to settle this issue before moving forward.
If the current MERGE PARTITION design is finalized, then v48-0001 looks solid.

[1] https://postgr.es/m/CA+TgmoY0=bT_xBP8csR=MFE=[email-masked]

---

From: Dmitry Koval <[email-masked]>
Date: Wed, 25 Jun 2025 00:28:28 +0300

Hi!
Thanks for notes!

1.
 >here, we don't need ``(void *)``

Corrected.

2.

Changed.

3.
 > after ...

Added.

4.
 >ALTER TABLE tab1 MERGE PARTITION part1 WITH (part2, part3) mentioned 
in [1].

Is it additional syntax (to the existing one) with functionality: 
partition part1 survives and data from partitions part2, part3 is moved 
into part1?
And (if "yes") should we delete the part1 indexes (or other constraints) 
before moving the data?

[1] 
https://postgr.es/m/CA+TgmoY0=bT_xBP8csR=MFE=[email-masked]

-- 
With best regards,
Dmitry Koval

Postgres Professional: http://postgrespro.com
From d5f7f509b97485970e970e3d5b1f9fba228f962c Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:57:22 +0300
Subject: [PATCH v49 1/2] Implement ALTER TABLE ... MERGE PARTITIONS ...
 command

 doc/src/sgml/ref/alter_table.sgml             |  113 +-
 src/backend/partitioning/partbounds.c         |  194 +++
 21 files changed, 3618 insertions(+), 26 deletions(-)

index d63f3a621a..dd429c12b1 100644
@@ -37,6 +37,8 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
+    MERGE PARTITIONS (<replaceable class="parameter">partition_name1</replaceable>, <replaceable class="parameter">partition_name2</replaceable> [, ...]) INTO <replaceable class="parameter">partition_name</replaceable>

@@ -1147,14 +1149,101 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>

   <para>
    All the forms of <command>ALTER TABLE</command> that act on a single table,
    except <literal>RENAME</literal>, <literal>SET SCHEMA</literal>,
-   <literal>ATTACH PARTITION</literal>, and
-   <literal>DETACH PARTITION</literal> can be combined into
+   <literal>ATTACH PARTITION</literal>, <literal>DETACH PARTITION</literal>,
+   and <literal>MERGE PARTITIONS</literal> can be combined into
    a list of multiple alterations to be applied together.  For example, it
    is possible to add several columns and/or alter the type of several
    columns in a single command.  This is particularly useful with large
@@ -1397,7 +1486,18 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       <term><replaceable class="parameter">partition_name</replaceable></term>
       <listitem>
        <para>
-        The name of the table to attach as a new partition or to detach from this table.
+        The name of the table to attach as a new partition or to detach from this table,
+        or the name of the new merged partition.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="sql-altertable-parms-partition-name1">
+      <term><replaceable class="parameter">partition_name1</replaceable></term>
+      <term><replaceable class="parameter">partition_name2</replaceable></term>
+      <listitem>
+       <para>
+        The names of the tables being merged into the new partition.
        </para>
       </listitem>
      </varlistentry>
@@ -1830,6 +1930,13 @@ ALTER TABLE measurement
     DETACH PARTITION measurement_y2015m12;
 </programlisting></para>
 
+  <para>
 
  <refsect1>
 
+/*
index 074ddb6b9c..34858ab509 100644

 
 
+/*

 	/*
 
 	return cstorage;
 }
+
+
+/*

 

+/*
+ * checkPartition

 				/*
diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index 4bdc2941ef..ea33c15194 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -4977,3 +4977,197 @@ satisfies_hash_partition(PG_FUNCTION_ARGS)
 
+						  qsort_partition_rbound_cmp, key);
index 908eef97c6..1145b9d7ce 100644

 

 

From f8a40867cf3d03899722cab3716d7374e69c65e9 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sun, 7 Apr 2024 00:58:09 +0300
Subject: [PATCH v49 2/2] Implement ALTER TABLE ... SPLIT PARTITION ... command

 20 files changed, 4448 insertions(+), 24 deletions(-)

    <sect3 id="ddl-partitioning-declarative-limitations">
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index dd429c12b1..7b143c6585 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -39,6 +39,10 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
     DETACH PARTITION <replaceable class="parameter">partition_name</replaceable> [ CONCURRENTLY | FINALIZE ]
 ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
     MERGE PARTITIONS (<replaceable class="parameter">partition_name1</replaceable>, <replaceable class="parameter">partition_name2</replaceable> [, ...]) INTO <replaceable class="parameter">partition_name</replaceable>
+ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
+    SPLIT PARTITION <replaceable class="parameter">partition_name</replaceable> INTO
+        (PARTITION <replaceable class="parameter">partition_name1</replaceable> { FOR VALUES <replaceable class="parameter">partition_bound_spec</replaceable> | DEFAULT },
+         PARTITION <replaceable class="parameter">partition_name2</replaceable> { FOR VALUES <replaceable class="parameter">partition_bound_spec</replaceable> | DEFAULT } [, ...])

@@ -1149,6 +1153,71 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>

@@ -1243,7 +1312,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
    All the forms of <command>ALTER TABLE</command> that act on a single table,
    except <literal>RENAME</literal>, <literal>SET SCHEMA</literal>,
    <literal>ATTACH PARTITION</literal>, <literal>DETACH PARTITION</literal>,
-   and <literal>MERGE PARTITIONS</literal> can be combined into
+   <literal>SPLIT PARTITION</literal>, and <literal>MERGE PARTITIONS</literal>
+   can be combined into
    a list of multiple alterations to be applied together.  For example, it
    is possible to add several columns and/or alter the type of several
    columns in a single command.  This is particularly useful with large
@@ -1487,7 +1557,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       <listitem>
        <para>
         The name of the table to attach as a new partition or to detach from this table,
-        or the name of the new merged partition.
+        or the name of split partition, or the name of the new merged partition.
        </para>
       </listitem>
      </varlistentry>
@@ -1497,7 +1567,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       <term><replaceable class="parameter">partition_name2</replaceable></term>
       <listitem>
        <para>
-        The names of the tables being merged into the new partition.
+        The names of the tables being merged into the new partition or split into
+        new partitions.
        </para>
       </listitem>
      </varlistentry>
@@ -1930,6 +2001,24 @@ ALTER TABLE measurement
     DETACH PARTITION measurement_y2015m12;
 </programlisting></para>
 
+  <para>
index 34858ab509..de73540334 100644

 

 
+partitions_list:
 
+/*

 
 /*
  * transformPartitionCmd

index ea33c15194..e1c1416b1e 100644
@@ -4983,15 +4983,21 @@ satisfies_hash_partition(PG_FUNCTION_ARGS)
@@ -5000,6 +5006,7 @@ check_two_partitions_bounds_range(Relation parent,
@@ -5021,7 +5028,7 @@ check_two_partitions_bounds_range(Relation parent,

 				/*
 
 	return buf->data;
 }
+
+/*
index 1145b9d7ce..e4d3c5c940 100644
 
 	/*

+/*