public inbox for [email protected]  
help / color / mirror / Atom feed
From: Dmitry Koval <[email protected]>
To: Alvaro Herrera <[email protected]>
To: vignesh C <[email protected]>
Cc: [email protected]
Subject: Re: Add SPLIT PARTITION/MERGE PARTITIONS commands
Date: Fri, 26 Jan 2024 20:08:08 +0300
Message-ID: <[email protected]> (raw)
In-Reply-To: <[email protected]>
References: <[email protected]>

git format-patch -4 HEAD -v 23

=============================

Thanks!

I excluded regression test "Test: split partition witch identity column" 
from script src/test/regress/sql/partition_split.sql because after 
commit [1] partitions cannot contain identity columns and queries

CREATE TABLE salesmans2_5(salesman_id INT GENERATED ALWAYS AS IDENTITY 
PRIMARY KEY, salesman_name VARCHAR(30));
ALTER TABLE salesmans ATTACH PARTITION salesmans2_5 FOR VALUES FROM (2) 
TO (5);

returns

ERROR:  table "salesmans2_5" being attached contains an identity column 
"salesman_id"
DETAIL:  The new partition may not contain an identity column.

[1] 
https://github.com/postgres/postgres/commit/699586315704a8268808e3bdba4cb5924a038c49
-- 
With best regards,
Dmitry Koval

Postgres Professional: http://postgrespro.com
From 3041dee1fa86363dbaed12cde1d38f061f5fda1b Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email protected]>
Date: Mon, 19 Sep 2022 16:06:03 +0300
Subject: [PATCH v23 1/4] ALTER TABLE MERGE PARTITIONS command

---
 src/backend/commands/tablecmds.c              | 351 ++++++++-
 src/backend/parser/gram.y                     |  22 +-
 src/backend/parser/parse_utilcmd.c            |  89 +++
 src/backend/partitioning/partbounds.c         | 207 +++++
 src/include/nodes/parsenodes.h                |  13 +
 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 | 736 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/partition_merge.sql      | 434 +++++++++++
 src/tools/pgindent/typedefs.list              |   1 +
 15 files changed, 2100 insertions(+), 19 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/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 68f658e834..a44a8156cf 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -662,6 +662,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
@@ -4733,6 +4735,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);
@@ -5158,6 +5164,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);
@@ -5548,6 +5559,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);
@@ -6536,6 +6555,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:
@@ -18812,6 +18833,37 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
 	}
 }
 
+/*
+ * attachPartitionTable: attach new partition to 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
  *
@@ -19014,23 +19066,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 partition to partitioned table. */
+	attachPartitionTable(wqueue, rel, attachrel, cmd->bound);
 
 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -20584,3 +20621,283 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+ * createPartitionTable: create table for new partition with given name
+ * (newPartName) like table (modelRelName)
+ *
+ * Emulates command: CREATE TABLE <newPartName> (LIKE <modelRelName>
+ * INCLUDING ALL EXCLUDING INDEXES)
+ */
+static void
+createPartitionTable(RangeVar *newPartName, RangeVar *modelRelName,
+					 AlterTableUtilityContext *context)
+{
+	CreateStmt *createStmt;
+	TableLikeClause *tlc;
+	PlannedStmt *wrapper;
+
+	createStmt = makeNode(CreateStmt);
+	createStmt->relation = newPartName;
+	createStmt->tableElts = NIL;
+	createStmt->inhRelations = NIL;
+	createStmt->constraints = NIL;
+	createStmt->options = NIL;
+	createStmt->oncommit = ONCOMMIT_NOOP;
+	createStmt->tablespacename = NULL;
+	createStmt->if_not_exists = false;
+
+	tlc = makeNode(TableLikeClause);
+	tlc->relation = modelRelName;
+
+	/*
+	 * Indexes will be inherited on "attach new partitions" stage, after data
+	 * moving.
+	 */
+	tlc->options = CREATE_TABLE_LIKE_ALL & ~CREATE_TABLE_LIKE_INDEXES;
+	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);
+}
+
+/*
+ * Struct with context of merged partition
+ */
+typedef struct MergedPartContext
+{
+	Relation	partRel;		/* relation for partition */
+}			MergedPartContext;
+
+/*
+ * moveMergedTablesRows: scan merged partitions (partContext) of partitioned
+ * table (rel) and move rows into new partition (newPartRel).
+ */
+static void
+moveMergedTablesRows(Relation rel, List *partContext, 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, partContext)
+	{
+		MergedPartContext *pc = (MergedPartContext *) lfirst(listptr);
+		TupleTableSlot *srcslot;
+		TupleConversionMap *tuple_map;
+		TableScanDesc scan;
+		Snapshot	snapshot;
+
+		/* Create tuple slot for new partition. */
+		srcslot = MakeSingleTupleTableSlot(RelationGetDescr(pc->partRel),
+										   table_slot_callbacks(pc->partRel));
+
+		/*
+		 * Map computing for moving attributes of merged partition to new
+		 * partition.
+		 */
+		tuple_map = convert_tuples_by_name(RelationGetDescr(pc->partRel),
+										   RelationGetDescr(newPartRel));
+
+		/* Scan through the rows. */
+		snapshot = RegisterSnapshot(GetLatestSnapshot());
+		scan = table_beginscan(pc->partRel, 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 for 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	   *partContexts = NIL;
+	Oid			defaultPartOid;
+	char		tmpRelName[NAMEDATALEN];
+	RangeVar   *mergePartName = cmd->name;
+	bool		isSameName = false;
+
+	/*
+	 * Lock all merged partitions, check them and create list with partitions
+	 * contexts.
+	 */
+	foreach(listptr, cmd->partlist)
+	{
+		RangeVar   *name = (RangeVar *) lfirst(listptr);
+		MergedPartContext *pc;
+
+		pc = (MergedPartContext *) palloc0(sizeof(MergedPartContext));
+
+		/*
+		 * We are going to detach and remove this partition: need to use
+		 * exclusive lock for prevent DML-queries to the partition.
+		 */
+		pc->partRel = table_openrv(name, AccessExclusiveLock);
+
+		if (pc->partRel->rd_rel->relkind != RELKIND_RELATION)
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("cannot merge non-table partition \"%s\"",
+							RelationGetRelationName(pc->partRel))));
+
+		/*
+		 * Checking that two partitions have the same name was before,
+		 * in function transformPartitionCmdForMerge().
+		 */
+		if (equal(name, cmd->name))
+			/* One new partition can have the same name as merged partition. */
+			isSameName = true;
+
+		/* Store partition context into partitions list. */
+		partContexts = lappend(partContexts, pc);
+	}
+
+	/* Detach all merged partitions. */
+	defaultPartOid =
+		get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true));
+	foreach(listptr, partContexts)
+	{
+		MergedPartContext *pc = (MergedPartContext *) lfirst(listptr);
+
+		RemoveInheritance(pc->partRel, rel, false);
+		/* Do the final part of detaching. */
+		DetachPartitionFinalize(rel, pc->partRel, false, defaultPartOid);
+	}
+
+	/* Create table for new partition, use partitioned table as model. */
+	if (isSameName)
+	{
+		/* Create partition table with generated temparary name. */
+		sprintf(tmpRelName, "merge-%u-%X-tmp", RelationGetRelid(rel), MyProcPid);
+		mergePartName = makeRangeVar(get_namespace_name(RelationGetNamespace(rel)),
+									 tmpRelName, -1);
+	}
+	createPartitionTable(mergePartName,
+						 makeRangeVar(get_namespace_name(RelationGetNamespace(rel)),
+									  RelationGetRelationName(rel), -1),
+						 context);
+
+	/* Open the new partition and acquire exclusive lock on it. */
+	newPartRel = table_openrv(mergePartName, AccessExclusiveLock);
+
+	/* Copy data from merged partitions to new partition. */
+	moveMergedTablesRows(rel, partContexts, newPartRel);
+
+	/*
+	 * Attach new partition to partitioned table. wqueue = NULL: verification
+	 * for each cloned constraint is not need.
+	 */
+	attachPartitionTable(NULL, rel, newPartRel, cmd->bound);
+
+	/* Unlock and drop merged partitions. */
+	foreach(listptr, partContexts)
+	{
+		ObjectAddress object;
+		MergedPartContext *pc = (MergedPartContext *) lfirst(listptr);
+
+		/* Get relation id before table_close() call. */
+		object.objectId = RelationGetRelid(pc->partRel);
+		object.classId = RelationRelationId;
+		object.objectSubId = 0;
+
+		/* Keep the lock until commit. */
+		table_close(pc->partRel, NoLock);
+
+		performDeletion(&object, DROP_RESTRICT, 0);
+
+		pfree(pc);
+	}
+
+	/* Rename new partition if it is needed. */
+	if (isSameName)
+	{
+		/*
+		 * We must bump the command counter to make the new partition tuple
+		 * visible for rename.
+		 */
+		CommandCounterIncrement();
+		/* Rename partition. */
+		RenameRelationInternal(RelationGetRelid(newPartRel),
+							   cmd->name->relname, false, false);
+	}
+	/* Keep the lock until commit. */
+	table_close(newPartRel, NoLock);
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 130f7fc7c3..4a5b13c300 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -744,7 +744,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
+	PARALLEL PARAMETER PARSER PARTIAL PARTITION PARTITIONS PASSING PASSWORD
 	PLACING PLANS POLICY
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
 	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
@@ -2299,6 +2299,7 @@ partition_cmd:
 					n->subtype = AT_AttachPartition;
 					cmd->name = $3;
 					cmd->bound = $4;
+					cmd->partlist = NULL;
 					cmd->concurrent = false;
 					n->def = (Node *) cmd;
 
@@ -2313,6 +2314,7 @@ partition_cmd:
 					n->subtype = AT_DetachPartition;
 					cmd->name = $3;
 					cmd->bound = NULL;
+					cmd->partlist = NULL;
 					cmd->concurrent = $4;
 					n->def = (Node *) cmd;
 
@@ -2326,6 +2328,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;
@@ -2342,6 +2359,7 @@ index_partition_cmd:
 					n->subtype = AT_AttachPartition;
 					cmd->name = $3;
 					cmd->bound = NULL;
+					cmd->partlist = NULL;
 					cmd->concurrent = false;
 					n->def = (Node *) cmd;
 
@@ -17324,6 +17342,7 @@ unreserved_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PLANS
@@ -17925,6 +17944,7 @@ bare_label_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PLACING
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 56ac4f516e..884a280869 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -58,6 +58,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"
@@ -3404,6 +3406,80 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
 }
 
 
+/*
+ * transformPartitionCmdForMerge
+ *		Analyze the ALTER TABLLE ... MERGE PARTITIONS command
+ *
+ * Does simple checks for merged partitions. Calculates bound of result
+ * 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\" 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;
+		partOids = lappend_oid(partOids, partOid);
+	}
+
+	/* Allocate bound of result 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
@@ -3674,6 +3750,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 contains 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 8a1a5a7255..b3b3aac02a 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -4978,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 equals except case
+ * "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 of 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 determine 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 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 a lower bound of merged
+				 * partition.
+				 */
+				spec->lowerdatums =
+					((PartitionBoundSpec *) list_nth(bounds, lower_bounds[0]->index))->lowerdatums;
+
+				/*
+				 * Upper bound of last partition is a 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/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 476d55dd24..edfe36bf42 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -924,6 +924,17 @@ typedef struct PartitionRangeDatum
 	int			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
  */
@@ -932,6 +943,7 @@ 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 SPLIT PARTITION command */
 	bool		concurrent;
 } PartitionCmd;
 
@@ -2248,6 +2260,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 2331acac09..985474df89 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -324,6 +324,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("placing", PLACING, RESERVED_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 b2be88ead1..1071256182 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -103,6 +103,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..ec48732c58
--- /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 trying to update inserted row and waits until s1 finished
+# 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 48563b2cf0..69920757b2 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..013ab65961
--- /dev/null
+++ b/src/test/regress/expected/partition_merge.out
@@ -0,0 +1,736 @@
+--
+-- PARTITIONS_MERGE
+-- Tests for "ALTER TABLE ... MERGE PARTITIONS ..." command
+--
+CREATE SCHEMA partitions_merge_schema;
+SET search_path = partitions_merge_schema, public;
+--
+-- BY RANGE partitioning
+--
+--
+-- Test for error codes
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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 (salesman_id INT, salesman_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" 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" already used
+LINE 1: ...e MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_feb2...
+                                                             ^
+-- ERROR:  cannot merge non-table partition "sales_apr2022"
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_apr2022) INTO sales_feb_mar_apr2022;
+ERROR:  cannot merge non-table partition "sales_apr2022"
+-- ERROR:  invalid partitions order, partition "sales_mar2022" can not be merged
+-- (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:  invalid partitions order, partition "sales_jan2022" can not be merged
+-- (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
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_jan2022) INTO sales_jan_feb_mar2022;
+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         | 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_jan_feb_mar2022 | r       | f                | FOR VALUES FROM ('01-01-2022') TO ('04-01-2022')
+ sales_others          | r       | f                | DEFAULT
+(4 rows)
+
+DROP TABLE sales_range;
+--
+-- Add rows into partitioned table then merge partitions
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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)
+
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_apr2022) INTO 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                    
+-----------------------+---------+------------------+--------------------------------------------------
+ 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_schema';
+       schemaname        |       tablename       |              indexname               | tablespace |                                                          indexdef                                                           
+-------------------------+-----------------------+--------------------------------------+------------+-----------------------------------------------------------------------------------------------------------------------------
+ partitions_merge_schema | sales_feb_mar_apr2022 | sales_feb_mar_apr2022_sales_date_idx |            | CREATE INDEX sales_feb_mar_apr2022_sales_date_idx ON partitions_merge_schema.sales_feb_mar_apr2022 USING btree (sales_date)
+(1 row)
+
+SELECT * FROM sales_range;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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_feb_mar_apr2022;
+ salesman_id | salesman_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;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+          14 | Smith         |          510 | 05-04-2022
+(1 row)
+
+-- Use indexscan for test indexes
+SET enable_indexscan = ON;
+SET enable_seqscan = OFF;
+SELECT * FROM sales_feb_mar_apr2022 where sales_date > '2022-01-01';
+ salesman_id | salesman_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)
+
+SET enable_indexscan = ON;
+SET enable_seqscan = ON;
+DROP TABLE sales_range;
+--
+-- Merge some partitions into DEFAULT partition
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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, sales_others) INTO sales_others;
+select * from sales_others;
+ salesman_id | salesman_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 (salesman_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(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2021, 12, 7);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2021, 12, 8);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 1, 1);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 2, 4);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 1, 2);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 2, 1);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 3, 3);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 3, 4);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 5, 1);
+SELECT * FROM sales_date;
+ salesman_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;
+ salesman_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;
+ salesman_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;
+ salesman_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;
+ salesman_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(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 1, 10);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 2, 10);
+SELECT * FROM sales_date;
+ salesman_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;
+ salesman_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;
+ salesman_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;
+ salesman_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 salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+CREATE TABLE salesmans01_10 PARTITION OF salesmans FOR VALUES FROM (1) TO (10);
+CREATE TABLE salesmans10_20 PARTITION OF salesmans FOR VALUES FROM (10) TO (20);
+CREATE TABLE salesmans20_30 PARTITION OF salesmans FOR VALUES FROM (20) TO (30);
+CREATE TABLE salesmans30_40 PARTITION OF salesmans FOR VALUES FROM (30) TO (40);
+INSERT INTO salesmans 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 salesmans_after_insert_statement_trigger
+    AFTER INSERT
+    ON salesmans
+    FOR EACH STATEMENT
+    EXECUTE PROCEDURE after_insert_row_trigger('salesmans');
+CREATE TRIGGER salesmans_after_insert_row_trigger
+    AFTER INSERT
+    ON salesmans
+    FOR EACH ROW
+    EXECUTE PROCEDURE after_insert_row_trigger('salesmans');
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salesmans VALUES (10, 'May');
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = ROW
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = STATEMENT
+-- 1 trigger should fire here (row):
+INSERT INTO salesmans10_20 VALUES (19, 'Ivanov');
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = ROW
+ALTER TABLE salesmans MERGE PARTITIONS (salesmans10_20, salesmans20_30, salesmans30_40) INTO salesmans10_40;
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salesmans VALUES (20, 'Smirnoff');
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = ROW
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = STATEMENT
+-- 1 trigger should fire here (row):
+INSERT INTO salesmans10_40 VALUES (30, 'Ford');
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = ROW
+SELECT * FROM salesmans01_10;
+ salesman_id | salesman_name 
+-------------+---------------
+           1 | Poirot
+(1 row)
+
+SELECT * FROM salesmans10_40;
+ salesman_id | salesman_name 
+-------------+---------------
+          10 | May
+          19 | Ivanov
+          20 | Smirnoff
+          30 | Ford
+(4 rows)
+
+DROP TABLE salesmans;
+DROP FUNCTION after_insert_row_trigger();
+--
+-- Test: merge partitions with deleted columns
+--
+CREATE TABLE salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+CREATE TABLE salesmans01_10 PARTITION OF salesmans FOR VALUES FROM (1) TO (10);
+-- Create partitions with some deleted columns:
+CREATE TABLE salesmans10_20(d1 VARCHAR(30), salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30));
+CREATE TABLE salesmans20_30(salesman_id INT PRIMARY KEY, d2 INT,  salesman_name VARCHAR(30));
+CREATE TABLE salesmans30_40(salesman_id INT PRIMARY KEY, d3 DATE, salesman_name VARCHAR(30));
+INSERT INTO salesmans10_20 VALUES ('dummy value 1', 19, 'Ivanov');
+INSERT INTO salesmans20_30 VALUES (20, 101, 'Smirnoff');
+INSERT INTO salesmans30_40 VALUES (31, now(), 'Popov');
+ALTER TABLE salesmans10_20 DROP COLUMN d1;
+ALTER TABLE salesmans20_30 DROP COLUMN d2;
+ALTER TABLE salesmans30_40 DROP COLUMN d3;
+ALTER TABLE salesmans ATTACH PARTITION salesmans10_20 FOR VALUES FROM (10) TO (20);
+ALTER TABLE salesmans ATTACH PARTITION salesmans20_30 FOR VALUES FROM (20) TO (30);
+ALTER TABLE salesmans ATTACH PARTITION salesmans30_40 FOR VALUES FROM (30) TO (40);
+INSERT INTO salesmans VALUES (1, 'Poirot');
+INSERT INTO salesmans VALUES (10, 'May');
+INSERT INTO salesmans VALUES (30, 'Ford');
+ALTER TABLE salesmans MERGE PARTITIONS (salesmans10_20, salesmans20_30, salesmans30_40) INTO salesmans10_40;
+select * from salesmans;
+ salesman_id | salesman_name 
+-------------+---------------
+           1 | Poirot
+          19 | Ivanov
+          10 | May
+          20 | Smirnoff
+          31 | Popov
+          30 | Ford
+(6 rows)
+
+select * from salesmans01_10;
+ salesman_id | salesman_name 
+-------------+---------------
+           1 | Poirot
+(1 row)
+
+select * from salesmans10_40;
+ salesman_id | salesman_name 
+-------------+---------------
+          19 | Ivanov
+          10 | May
+          20 | Smirnoff
+          31 | Popov
+          30 | Ford
+(5 rows)
+
+DROP TABLE salesmans;
+--
+-- Test: merge sub-partitions
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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 (salesman_id INT, salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+           4 | Ivanov        |          750 | 04-13-2022
+(1 row)
+
+SELECT * FROM sales_apr2022_20_30;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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
+(salesman_id INT GENERATED ALWAYS AS IDENTITY,
+ salesman_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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_west PARTITION OF sales_list FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk');
+CREATE TABLE sales_east PARTITION OF sales_list FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok');
+CREATE TABLE sales_central PARTITION OF sales_list FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd');
+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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_others2 PARTITION OF sales_list2 DEFAULT;
+CREATE TABLE sales_external (LIKE sales_list);
+CREATE TABLE sales_external2 (vch VARCHAR(5));
+-- ERROR:  partition bound for relation "sales_external" is null
+ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_external) INTO sales_all;
+ERROR:  partition bound for relation "sales_external" is null
+-- ERROR:  partition bound for relation "sales_external2" is null
+ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_external2) INTO sales_all;
+ERROR:  partition bound for relation "sales_external2" is null
+-- 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
+(salesman_id INT GENERATED ALWAYS AS IDENTITY,
+ salesman_name VARCHAR(30),
+ sales_state VARCHAR(20),
+ sales_amount INT,
+ sales_date DATE)
+PARTITION BY LIST (sales_state);
+CREATE INDEX sales_list_salesman_name_idx ON sales_list USING btree (salesman_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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_west PARTITION OF sales_list FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk');
+CREATE TABLE sales_east PARTITION OF sales_list FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok');
+CREATE TABLE sales_central PARTITION OF sales_list FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd');
+CREATE TABLE sales_others PARTITION OF sales_list DEFAULT;
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Trump',    'Magadan',        1000, '2022-03-01');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Smirnoff', 'Smolensk',       500,  '2022-03-03');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Ford',     'St. Petersburg', 2000, '2022-03-05');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Ivanov',   'Moscow',         750,  '2022-03-04');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Deev',     'Voronezh',       250,  '2022-03-07');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Poirot',   'Kazan',          1000, '2022-03-01');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Ukhta',          1200, '2022-03-06');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Li',       'Vladivostok',    1150, '2022-03-09');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Ukhta',          1200, '2022-03-11');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Halder',   'Murmansk',       800,  '2022-03-02');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Muller',   'Bryansk',        650,  '2022-03-05');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Smith',    'Volgograd',      350,  '2022-03-10');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Gandi',    'Moscow',         150,  '2022-03-08');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Plato',    'Voronezh',       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 ('Moscow', 'Kazan', 'Volgograd')
+ sales_east    | r       | f                | FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok')
+ sales_nord    | r       | f                | FOR VALUES IN ('Murmansk', 'St. Petersburg', 'Ukhta')
+ sales_west    | r       | f                | FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk')
+ 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 ('Voronezh', 'Smolensk', 'Bryansk', 'Magadan', 'Khabarovsk', 'Vladivostok', 'Moscow', 'Kazan', 'Volgograd')
+ sales_nord   | r       | f                | FOR VALUES IN ('Murmansk', 'St. Petersburg', 'Ukhta')
+ sales_others | r       | f                | DEFAULT
+(3 rows)
+
+SELECT * FROM sales_list;
+ salesman_id | salesman_name |  sales_state   | sales_amount | sales_date 
+-------------+---------------+----------------+--------------+------------
+           2 | Smirnoff      | Smolensk       |          500 | 03-03-2022
+           5 | Deev          | Voronezh       |          250 | 03-07-2022
+          11 | Muller        | Bryansk        |          650 | 03-05-2022
+          14 | Plato         | Voronezh       |          950 | 03-05-2022
+           1 | Trump         | Magadan        |         1000 | 03-01-2022
+           8 | Li            | Vladivostok    |         1150 | 03-09-2022
+           4 | Ivanov        | Moscow         |          750 | 03-04-2022
+           6 | Poirot        | Kazan          |         1000 | 03-01-2022
+          12 | Smith         | Volgograd      |          350 | 03-10-2022
+          13 | Gandi         | Moscow         |          150 | 03-08-2022
+           3 | Ford          | St. Petersburg |         2000 | 03-05-2022
+           7 | May           | Ukhta          |         1200 | 03-06-2022
+           9 | May           | Ukhta          |         1200 | 03-11-2022
+          10 | Halder        | Murmansk       |          800 | 03-02-2022
+(14 rows)
+
+SELECT * FROM sales_nord;
+ salesman_id | salesman_name |  sales_state   | sales_amount | sales_date 
+-------------+---------------+----------------+--------------+------------
+           3 | Ford          | St. Petersburg |         2000 | 03-05-2022
+           7 | May           | Ukhta          |         1200 | 03-06-2022
+           9 | May           | Ukhta          |         1200 | 03-11-2022
+          10 | Halder        | Murmansk       |          800 | 03-02-2022
+(4 rows)
+
+SELECT * FROM sales_all;
+ salesman_id | salesman_name | sales_state | sales_amount | sales_date 
+-------------+---------------+-------------+--------------+------------
+           2 | Smirnoff      | Smolensk    |          500 | 03-03-2022
+           5 | Deev          | Voronezh    |          250 | 03-07-2022
+          11 | Muller        | Bryansk     |          650 | 03-05-2022
+          14 | Plato         | Voronezh    |          950 | 03-05-2022
+           1 | Trump         | Magadan     |         1000 | 03-01-2022
+           8 | Li            | Vladivostok |         1150 | 03-09-2022
+           4 | Ivanov        | Moscow      |          750 | 03-04-2022
+           6 | Poirot        | Kazan       |         1000 | 03-01-2022
+          12 | Smith         | Volgograd   |          350 | 03-10-2022
+          13 | Gandi         | Moscow      |          150 | 03-08-2022
+(10 rows)
+
+-- Use indexscan for test indexes after merge partitions
+SET enable_indexscan = ON;
+SET enable_seqscan = OFF;
+SELECT * FROM sales_all WHERE sales_state = 'Moscow';
+ salesman_id | salesman_name | sales_state | sales_amount | sales_date 
+-------------+---------------+-------------+--------------+------------
+           4 | Ivanov        | Moscow      |          750 | 03-04-2022
+          13 | Gandi         | Moscow      |          150 | 03-08-2022
+(2 rows)
+
+SELECT * FROM sales_list WHERE sales_state = 'Moscow';
+ salesman_id | salesman_name | sales_state | sales_amount | sales_date 
+-------------+---------------+-------------+--------------+------------
+           4 | Ivanov        | Moscow      |          750 | 03-04-2022
+          13 | Gandi         | Moscow      |          150 | 03-08-2022
+(2 rows)
+
+SELECT * FROM sales_list WHERE salesman_name = 'Ivanov';
+ salesman_id | salesman_name | sales_state | sales_amount | sales_date 
+-------------+---------------+-------------+--------------+------------
+           4 | Ivanov        | Moscow      |          750 | 03-04-2022
+(1 row)
+
+SET enable_indexscan = ON;
+SET enable_seqscan = ON;
+DROP TABLE sales_list;
+--
+DROP SCHEMA partitions_merge_schema;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 1d8a414eea..6f0236d207 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..8bf90fd621
--- /dev/null
+++ b/src/test/regress/sql/partition_merge.sql
@@ -0,0 +1,434 @@
+--
+-- PARTITIONS_MERGE
+-- Tests for "ALTER TABLE ... MERGE PARTITIONS ..." command
+--
+
+CREATE SCHEMA partitions_merge_schema;
+SET search_path = partitions_merge_schema, public;
+
+--
+-- BY RANGE partitioning
+--
+
+--
+-- Test for error codes
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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 (salesman_id INT, salesman_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" already used
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_feb2022) INTO sales_feb_mar_apr2022;
+-- ERROR:  cannot merge non-table partition "sales_apr2022"
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_apr2022) INTO sales_feb_mar_apr2022;
+-- ERROR:  invalid partitions order, partition "sales_mar2022" can not be merged
+-- (space between sections sales_jan2022 and sales_mar2022)
+ALTER TABLE sales_range MERGE PARTITIONS (sales_jan2022, sales_mar2022) INTO sales_jan_mar2022;
+-- ERROR:  invalid partitions order, partition "sales_jan2022" can not be merged
+-- (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
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_jan2022) INTO sales_jan_feb_mar2022;
+
+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 (salesman_id INT, salesman_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;
+
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_apr2022) INTO 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_schema';
+
+SELECT * FROM sales_range;
+SELECT * FROM sales_jan2022;
+SELECT * FROM sales_feb_mar_apr2022;
+SELECT * FROM sales_others;
+
+-- Use indexscan for test indexes
+SET enable_indexscan = ON;
+SET enable_seqscan = OFF;
+
+SELECT * FROM sales_feb_mar_apr2022 where sales_date > '2022-01-01';
+
+SET enable_indexscan = ON;
+SET enable_seqscan = ON;
+
+DROP TABLE sales_range;
+
+--
+-- Merge some partitions into DEFAULT partition
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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, 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 (salesman_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(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2021, 12, 7);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2021, 12, 8);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 1, 1);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 2, 4);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 1, 2);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 2, 1);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 3, 3);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 3, 4);
+INSERT INTO sales_date(salesman_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(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 1, 10);
+INSERT INTO sales_date(salesman_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 salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+
+CREATE TABLE salesmans01_10 PARTITION OF salesmans FOR VALUES FROM (1) TO (10);
+CREATE TABLE salesmans10_20 PARTITION OF salesmans FOR VALUES FROM (10) TO (20);
+CREATE TABLE salesmans20_30 PARTITION OF salesmans FOR VALUES FROM (20) TO (30);
+CREATE TABLE salesmans30_40 PARTITION OF salesmans FOR VALUES FROM (30) TO (40);
+
+INSERT INTO salesmans 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 salesmans_after_insert_statement_trigger
+    AFTER INSERT
+    ON salesmans
+    FOR EACH STATEMENT
+    EXECUTE PROCEDURE after_insert_row_trigger('salesmans');
+
+CREATE TRIGGER salesmans_after_insert_row_trigger
+    AFTER INSERT
+    ON salesmans
+    FOR EACH ROW
+    EXECUTE PROCEDURE after_insert_row_trigger('salesmans');
+
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salesmans VALUES (10, 'May');
+-- 1 trigger should fire here (row):
+INSERT INTO salesmans10_20 VALUES (19, 'Ivanov');
+
+ALTER TABLE salesmans MERGE PARTITIONS (salesmans10_20, salesmans20_30, salesmans30_40) INTO salesmans10_40;
+
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salesmans VALUES (20, 'Smirnoff');
+-- 1 trigger should fire here (row):
+INSERT INTO salesmans10_40 VALUES (30, 'Ford');
+
+SELECT * FROM salesmans01_10;
+SELECT * FROM salesmans10_40;
+
+DROP TABLE salesmans;
+DROP FUNCTION after_insert_row_trigger();
+
+--
+-- Test: merge partitions with deleted columns
+--
+CREATE TABLE salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+
+CREATE TABLE salesmans01_10 PARTITION OF salesmans FOR VALUES FROM (1) TO (10);
+-- Create partitions with some deleted columns:
+CREATE TABLE salesmans10_20(d1 VARCHAR(30), salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30));
+CREATE TABLE salesmans20_30(salesman_id INT PRIMARY KEY, d2 INT,  salesman_name VARCHAR(30));
+CREATE TABLE salesmans30_40(salesman_id INT PRIMARY KEY, d3 DATE, salesman_name VARCHAR(30));
+
+INSERT INTO salesmans10_20 VALUES ('dummy value 1', 19, 'Ivanov');
+INSERT INTO salesmans20_30 VALUES (20, 101, 'Smirnoff');
+INSERT INTO salesmans30_40 VALUES (31, now(), 'Popov');
+
+ALTER TABLE salesmans10_20 DROP COLUMN d1;
+ALTER TABLE salesmans20_30 DROP COLUMN d2;
+ALTER TABLE salesmans30_40 DROP COLUMN d3;
+
+ALTER TABLE salesmans ATTACH PARTITION salesmans10_20 FOR VALUES FROM (10) TO (20);
+ALTER TABLE salesmans ATTACH PARTITION salesmans20_30 FOR VALUES FROM (20) TO (30);
+ALTER TABLE salesmans ATTACH PARTITION salesmans30_40 FOR VALUES FROM (30) TO (40);
+
+INSERT INTO salesmans VALUES (1, 'Poirot');
+INSERT INTO salesmans VALUES (10, 'May');
+INSERT INTO salesmans VALUES (30, 'Ford');
+
+ALTER TABLE salesmans MERGE PARTITIONS (salesmans10_20, salesmans20_30, salesmans30_40) INTO salesmans10_40;
+
+select * from salesmans;
+select * from salesmans01_10;
+select * from salesmans10_40;
+
+DROP TABLE salesmans;
+
+--
+-- Test: merge sub-partitions
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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 (salesman_id INT, salesman_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
+(salesman_id INT GENERATED ALWAYS AS IDENTITY,
+ salesman_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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_west PARTITION OF sales_list FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk');
+CREATE TABLE sales_east PARTITION OF sales_list FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok');
+CREATE TABLE sales_central PARTITION OF sales_list FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd');
+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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_others2 PARTITION OF sales_list2 DEFAULT;
+
+
+CREATE TABLE sales_external (LIKE sales_list);
+CREATE TABLE sales_external2 (vch VARCHAR(5));
+
+-- ERROR:  partition bound for relation "sales_external" is null
+ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_external) INTO sales_all;
+-- ERROR:  partition bound for relation "sales_external2" is null
+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
+(salesman_id INT GENERATED ALWAYS AS IDENTITY,
+ salesman_name VARCHAR(30),
+ sales_state VARCHAR(20),
+ sales_amount INT,
+ sales_date DATE)
+PARTITION BY LIST (sales_state);
+
+CREATE INDEX sales_list_salesman_name_idx ON sales_list USING btree (salesman_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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_west PARTITION OF sales_list FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk');
+CREATE TABLE sales_east PARTITION OF sales_list FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok');
+CREATE TABLE sales_central PARTITION OF sales_list FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd');
+CREATE TABLE sales_others PARTITION OF sales_list DEFAULT;
+
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Trump',    'Magadan',        1000, '2022-03-01');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Smirnoff', 'Smolensk',       500,  '2022-03-03');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Ford',     'St. Petersburg', 2000, '2022-03-05');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Ivanov',   'Moscow',         750,  '2022-03-04');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Deev',     'Voronezh',       250,  '2022-03-07');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Poirot',   'Kazan',          1000, '2022-03-01');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Ukhta',          1200, '2022-03-06');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Li',       'Vladivostok',    1150, '2022-03-09');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Ukhta',          1200, '2022-03-11');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Halder',   'Murmansk',       800,  '2022-03-02');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Muller',   'Bryansk',        650,  '2022-03-05');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Smith',    'Volgograd',      350,  '2022-03-10');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Gandi',    'Moscow',         150,  '2022-03-08');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Plato',    'Voronezh',       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 test indexes after merge partitions
+SET enable_indexscan = ON;
+SET enable_seqscan = OFF;
+
+SELECT * FROM sales_all WHERE sales_state = 'Moscow';
+SELECT * FROM sales_list WHERE sales_state = 'Moscow';
+SELECT * FROM sales_list WHERE salesman_name = 'Ivanov';
+
+SET enable_indexscan = ON;
+SET enable_seqscan = ON;
+
+DROP TABLE sales_list;
+
+--
+DROP SCHEMA partitions_merge_schema;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 7e866e3c3d..e827ed0214 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2578,6 +2578,7 @@ SimpleStats
 SimpleStringList
 SimpleStringListCell
 SingleBoundSortItem
+SinglePartitionSpec
 Size
 SkipPages
 SlabBlock
-- 
2.40.1.windows.1


From 2439724753c8ac6201685a3d34964f9af256aabf Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email protected]>
Date: Mon, 19 Sep 2022 17:58:52 +0300
Subject: [PATCH v23 2/4] ALTER TABLE SPLIT PARTITION command

---
 src/backend/commands/tablecmds.c              |  407 +++++
 src/backend/parser/gram.y                     |   38 +-
 src/backend/parser/parse_utilcmd.c            |   62 +-
 src/backend/partitioning/partbounds.c         |  655 ++++++++
 src/backend/utils/adt/ruleutils.c             |   18 +
 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 | 1317 +++++++++++++++++
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/partition_split.sql      |  792 ++++++++++
 16 files changed, 3536 insertions(+), 12 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/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index a44a8156cf..b0d2e4af91 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -662,6 +662,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);
 
@@ -4735,6 +4738,10 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = ShareUpdateExclusiveLock;
 				break;
 
+			case AT_SplitPartition:
+				cmd_lockmode = AccessExclusiveLock;
+				break;
+
 			case AT_MergePartitions:
 				cmd_lockmode = AccessExclusiveLock;
 				break;
@@ -5164,6 +5171,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 */
@@ -5559,6 +5571,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);
@@ -6555,6 +6575,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:
@@ -20622,6 +20644,255 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 	return cstorage;
 }
 
+/*
+ * Struct with context of new partition for insert rows from splited partition
+ */
+typedef struct SplitPartitionContext
+{
+	ExprState  *partqualstate;	/* expression for check slot for partition
+								 * (NULL for DEFAULT partition) */
+	BulkInsertState bistate;	/* state of bulk inserts for partition */
+	TupleTableSlot *dstslot;	/* slot for insert 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 Relation's.
+ * 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 for 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 new partition with given name
  * (newPartName) like table (modelRelName)
@@ -20676,6 +20947,142 @@ createPartitionTable(RangeVar *newPartName, RangeVar *modelRelName,
 				   NULL);
 }
 
+/*
+ * 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;
+	RangeVar   *splitPartName = cmd->name;
+	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 prevent DML-queries to the partition.
+	 */
+	splitRel = table_openrv(splitPartName, AccessExclusiveLock);
+
+	if (splitRel->rd_rel->relkind != RELKIND_RELATION)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("cannot split non-table partition \"%s\"",
+						RelationGetRelationName(splitRel))));
+
+	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.
+		 */
+		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 reuse name.
+	 */
+	if (isSameName)
+	{
+		/*
+		 * We must bump the command counter to make the split partition tuple
+		 * visible for rename.
+		 */
+		CommandCounterIncrement();
+		/* Rename partition. */
+		sprintf(tmpRelName, "split-%u-%X-tmp", RelationGetRelid(rel), MyProcPid);
+		RenameRelationInternal(splitRelOid, tmpRelName, false, false);
+		splitPartName = makeRangeVar(
+									 get_namespace_name(RelationGetNamespace(splitRel)),
+									 tmpRelName, -1);
+
+		/*
+		 * We must bump the command counter to make the split partition tuple
+		 * visible after rename.
+		 */
+		CommandCounterIncrement();
+	}
+
+	/* Create new partitions (like split partition), without indexes. */
+	foreach(listptr, cmd->partlist)
+	{
+		SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
+		Relation	newPartRel;
+
+		createPartitionTable(sps->name, splitPartName, context);
+
+		/* Open the new partition and acquire exclusive lock on it. */
+		newPartRel = table_openrv(sps->name, AccessExclusiveLock);
+
+		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 need. */
+		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);
+}
+
 /*
  * Struct with context of merged partition
  */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4a5b13c300..6da1f9a076 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -270,6 +270,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;
@@ -643,6 +644,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
 
@@ -759,7 +762,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 SQL_P STABLE STANDALONE_P
+	SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SPLIT SQL_P STABLE STANDALONE_P
 	START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRIP_P
 	SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER
 
@@ -2289,6 +2292,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
@@ -2333,6 +2353,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
 				{
@@ -17407,6 +17441,7 @@ unreserved_keyword:
 			| SIMPLE
 			| SKIP
 			| SNAPSHOT
+			| SPLIT
 			| SQL_P
 			| STABLE
 			| STANDALONE_P
@@ -18021,6 +18056,7 @@ bare_label_keyword:
 			| SMALLINT
 			| SNAPSHOT
 			| SOME
+			| SPLIT
 			| SQL_P
 			| STABLE
 			| STANDALONE_P
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 884a280869..c5a794e237 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);
@@ -3406,6 +3406,43 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
 }
 
 
+/*
+ * transformPartitionCmdForSplit
+ *		Analyze the ALTER TABLLE ... 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);
+
+	/* Then we should check partitions with transformed bounds. */
+	check_partitions_for_split(parent, splitPartOid, partcmd->name, partcmd->partlist, cxt->pstate);
+}
+
+
 /*
  * transformPartitionCmdForMerge
  *		Analyze the ALTER TABLLE ... MERGE PARTITIONS command
@@ -3742,7 +3779,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;
 				}
@@ -3750,6 +3787,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 				newcmds = lappend(newcmds, cmd);
 				break;
 
+			case AT_SplitPartition:
 			case AT_MergePartitions:
 				{
 					PartitionCmd *partcmd = (PartitionCmd *) cmd->def;
@@ -3758,7 +3796,11 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 						ereport(ERROR,
 								(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 								 errmsg("list of new partitions should contains at least two items")));
-					transformPartitionCmdForMerge(&cxt, partcmd);
+
+					if (cmd->subtype == AT_SplitPartition)
+						transformPartitionCmdForSplit(&cxt, partcmd);
+					else
+						transformPartitionCmdForMerge(&cxt, partcmd);
 					newcmds = lappend(newcmds, cmd);
 					break;
 				}
@@ -4163,13 +4205,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;
 
@@ -4178,9 +4220,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:
 
@@ -4188,7 +4230,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 b3b3aac02a..e3504bfe26 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,602 @@ 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" is 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 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);
+		bool		overlap = false;
+
+		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 && cmpval) || (defaultPart && cmpval < 0))
+				overlap = true;
+		}
+		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 of "spec" should be equal (or less than or equal
+			 * in case defaultPart=true) to upper bound of split partition.
+			 */
+			if ((!defaultPart && cmpval) || (defaultPart && cmpval > 0))
+				overlap = true;
+		}
+
+		if (overlap)
+		{
+			PartitionRangeDatum *datum;
+
+			datum = list_nth(first ? spec->lowerdatums : spec->upperdatums, abs(cmpval) - 1);
+
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("%s bound of partition \"%s\" is %s %s bound of split partition",
+							first ? "lower" : "upper",
+							relname,
+							defaultPart ? (first ? "less than" : "greater than") : "not equals to",
+							first ? "lower" : "upper"),
+					 parser_errposition(pstate, datum->location)));
+		}
+	}
+}
+
+/*
+ * check_partition_bounds_for_split_list
+ *
+ * (function for BY LIST partitioning)
+ *
+ * Checks that bounds of new partition is 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
+ *
+ * (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 it contains
+	 * split partition (partOid).
+	 */
+	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
+	 * partitionde 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 get 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 not have value %s but split partition has",
+						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 = -1;
+				foreach(listptr, partlist)
+				{
+					SinglePartitionSpec *sps =
+					(SinglePartitionSpec *) lfirst(listptr);
+
+					i++;
+					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;
+						continue;
+					}
+					new_parts[nparts++] = sps;
+				}
+			}
+			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("any 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\" 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 b625f471a8..110c267823 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -12653,3 +12653,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/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index edfe36bf42..d0ffa3e9de 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2260,6 +2260,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 985474df89..89e0a5c504 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -404,6 +404,7 @@ PG_KEYWORD("skip", SKIP, UNRESERVED_KEYWORD, BARE_LABEL)
 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("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 1071256182..79b730fb86 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -104,6 +104,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 69920757b2..265ef2a547 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..38736d3e67
--- /dev/null
+++ b/src/test/regress/expected/partition_split.out
@@ -0,0 +1,1317 @@
+--
+-- PARTITION_SPLIT
+-- Tests for "ALTER TABLE ... SPLIT PARTITION ..." command
+--
+CREATE SCHEMA partition_split_schema;
+SET search_path = partition_split_schema, public;
+--
+-- BY RANGE partitioning
+--
+--
+-- Test for error codes
+--
+CREATE TABLE sales_range (salesman_id int, salesman_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 contains 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 contains 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" 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" already used
+LINE 3:    PARTITION sales_feb_mar_apr2022 FOR VALUES FROM ('2022-03...
+                     ^
+-- ERROR:  name "sales_feb2022" 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" 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" is not equals to upper bound of previous 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-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 equals 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 equals to lower bound of split partition
+LINE 2:   (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-02') TO...
+                                                    ^
+DROP TABLE sales_range;
+DROP TABLE sales_others;
+--
+-- Add rows into partitioned table then split partition
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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 (salesman_id INT, salesman_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;
+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'));
+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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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 (salesman_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(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2021, 12, 7);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2021, 12, 8);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 1, 1);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 2, 4);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 1, 2);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 2, 1);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 3, 3);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 3, 4);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 5, 1);
+SELECT * FROM sales_date;
+ salesman_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;
+ salesman_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;
+ salesman_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;
+ salesman_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(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 1, 10);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 2, 10);
+SELECT * FROM sales_date;
+ salesman_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;
+ salesman_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;
+ salesman_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;
+ salesman_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;
+ salesman_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; using a index on partition key; check index after split
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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;
+ salesman_id | salesman_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';
+       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 test indexes
+SET enable_indexscan = ON;
+SET enable_seqscan = OFF;
+SELECT * FROM sales_feb2022 where sales_date > '2022-01-01';
+ salesman_id | salesman_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';
+ salesman_id | salesman_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';
+ salesman_id | salesman_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';
+ salesman_id | salesman_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';
+       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';
+       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';
+       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';
+       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 split DEFAULT partition (different bounds)
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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 (exact 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:  any partition 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:  any 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 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 (salesman_id INT, salesman_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 equals 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 salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30));
+INSERT INTO salesmans VALUES (1,  'Poirot');
+CREATE TABLE sales_range (
+salesman_id INT REFERENCES salesmans(salesman_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;
+                    pg_get_constraintdef                     |            conname             | conkey 
+-------------------------------------------------------------+--------------------------------+--------
+ CHECK ((sales_amount > 1))                                  | sales_range_sales_amount_check | {2}
+ FOREIGN KEY (salesman_id) REFERENCES salesmans(salesman_id) | sales_range_salesman_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;
+                    pg_get_constraintdef                     |            conname             | conkey 
+-------------------------------------------------------------+--------------------------------+--------
+ CHECK ((sales_amount > 1))                                  | sales_range_sales_amount_check | {2}
+ FOREIGN KEY (salesman_id) REFERENCES salesmans(salesman_id) | sales_range_salesman_id_fkey   | {1}
+(2 rows)
+
+SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_mar2022'::regclass::oid;
+                    pg_get_constraintdef                     |            conname             | conkey 
+-------------------------------------------------------------+--------------------------------+--------
+ CHECK ((sales_amount > 1))                                  | sales_range_sales_amount_check | {2}
+ FOREIGN KEY (salesman_id) REFERENCES salesmans(salesman_id) | sales_range_salesman_id_fkey   | {1}
+(2 rows)
+
+SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_apr2022'::regclass::oid;
+                    pg_get_constraintdef                     |            conname             | conkey 
+-------------------------------------------------------------+--------------------------------+--------
+ CHECK ((sales_amount > 1))                                  | sales_range_sales_amount_check | {2}
+ FOREIGN KEY (salesman_id) REFERENCES salesmans(salesman_id) | sales_range_salesman_id_fkey   | {1}
+(2 rows)
+
+-- ERROR:  new row for relation "sales_mar2022" violates check constraint "sales_range_salesman_id_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_salesman_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_salesman_id_fkey"
+DETAIL:  Key (salesman_id)=(-1) is not present in table "salesmans".
+-- ok
+INSERT INTO sales_range VALUES (1, 10, '2022-03-11');
+DROP TABLE sales_range CASCADE;
+DROP TABLE salesmans CASCADE;
+--
+-- Test: split partition on partitioned table in case exists FOREIGN KEY reference from another table
+--
+CREATE TABLE salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+CREATE TABLE sales (salesman_id INT REFERENCES salesmans(salesman_id), sales_amount INT, sales_date DATE);
+CREATE TABLE salesmans01_10 PARTITION OF salesmans FOR VALUES FROM (1) TO (10);
+CREATE TABLE salesmans10_40 PARTITION OF salesmans FOR VALUES FROM (10) TO (40);
+INSERT INTO salesmans VALUES (1,  'Poirot');
+INSERT INTO salesmans VALUES (10, 'May');
+INSERT INTO salesmans VALUES (19, 'Ivanov');
+INSERT INTO salesmans VALUES (20, 'Smirnoff');
+INSERT INTO salesmans 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 salesmans01_10;
+ salesman_id | salesman_name 
+-------------+---------------
+           1 | Poirot
+(1 row)
+
+SELECT * FROM salesmans10_40;
+ salesman_id | salesman_name 
+-------------+---------------
+          10 | May
+          19 | Ivanov
+          20 | Smirnoff
+          30 | Ford
+(4 rows)
+
+ALTER TABLE salesmans SPLIT PARTITION salesmans10_40 INTO
+  (PARTITION salesmans10_20 FOR VALUES FROM (10) TO (20),
+   PARTITION salesmans20_30 FOR VALUES FROM (20) TO (30),
+   PARTITION salesmans30_40 FOR VALUES FROM (30) TO (40));
+SELECT * FROM salesmans01_10;
+ salesman_id | salesman_name 
+-------------+---------------
+           1 | Poirot
+(1 row)
+
+SELECT * FROM salesmans10_20;
+ salesman_id | salesman_name 
+-------------+---------------
+          10 | May
+          19 | Ivanov
+(2 rows)
+
+SELECT * FROM salesmans20_30;
+ salesman_id | salesman_name 
+-------------+---------------
+          20 | Smirnoff
+(1 row)
+
+SELECT * FROM salesmans30_40;
+ salesman_id | salesman_name 
+-------------+---------------
+          30 | Ford
+(1 row)
+
+-- ERROR:  insert or update on table "sales" violates foreign key constraint "sales_salesman_id_fkey"
+INSERT INTO sales VALUES (40, 50,  '2022-03-04');
+ERROR:  insert or update on table "sales" violates foreign key constraint "sales_salesman_id_fkey"
+DETAIL:  Key (salesman_id)=(40) is not present in table "salesmans".
+-- ok
+INSERT INTO sales VALUES (30, 50,  '2022-03-04');
+DROP TABLE sales CASCADE;
+DROP TABLE salesmans CASCADE;
+--
+-- Test: split partition of partitioned table with triggers
+--
+CREATE TABLE salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+CREATE TABLE salesmans01_10 PARTITION OF salesmans FOR VALUES FROM (1) TO (10);
+CREATE TABLE salesmans10_40 PARTITION OF salesmans FOR VALUES FROM (10) TO (40);
+INSERT INTO salesmans 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 salesmans_after_insert_statement_trigger
+    AFTER INSERT
+    ON salesmans
+    FOR EACH STATEMENT
+    EXECUTE PROCEDURE after_insert_row_trigger('salesmans');
+CREATE TRIGGER salesmans_after_insert_row_trigger
+    AFTER INSERT
+    ON salesmans
+    FOR EACH ROW
+    EXECUTE PROCEDURE after_insert_row_trigger('salesmans');
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salesmans VALUES (10, 'May');
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = ROW
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = STATEMENT
+-- 1 trigger should fire here (row):
+INSERT INTO salesmans10_40 VALUES (19, 'Ivanov');
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = ROW
+ALTER TABLE salesmans SPLIT PARTITION salesmans10_40 INTO
+  (PARTITION salesmans10_20 FOR VALUES FROM (10) TO (20),
+   PARTITION salesmans20_30 FOR VALUES FROM (20) TO (30),
+   PARTITION salesmans30_40 FOR VALUES FROM (30) TO (40));
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salesmans VALUES (20, 'Smirnoff');
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = ROW
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = STATEMENT
+-- 1 trigger should fire here (row):
+INSERT INTO salesmans30_40 VALUES (30, 'Ford');
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = ROW
+SELECT * FROM salesmans01_10;
+ salesman_id | salesman_name 
+-------------+---------------
+           1 | Poirot
+(1 row)
+
+SELECT * FROM salesmans10_20;
+ salesman_id | salesman_name 
+-------------+---------------
+          10 | May
+          19 | Ivanov
+(2 rows)
+
+SELECT * FROM salesmans20_30;
+ salesman_id | salesman_name 
+-------------+---------------
+          20 | Smirnoff
+(1 row)
+
+SELECT * FROM salesmans30_40;
+ salesman_id | salesman_name 
+-------------+---------------
+          30 | Ford
+(1 row)
+
+DROP TABLE salesmans CASCADE;
+DROP FUNCTION after_insert_row_trigger();
+--
+-- Test: split partition with deleted columns
+--
+CREATE TABLE salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+CREATE TABLE salesmans01_10 PARTITION OF salesmans FOR VALUES FROM (1) TO (10);
+-- Create new partition with some deleted columns:
+CREATE TABLE salesmans10_40(d1 VARCHAR(30), salesman_id INT PRIMARY KEY, d2 INT, d3 DATE, salesman_name VARCHAR(30));
+INSERT INTO salesmans10_40 VALUES ('dummy value 1', 19, 100, now(), 'Ivanov');
+INSERT INTO salesmans10_40 VALUES ('dummy value 2', 20, 101, now(), 'Smirnoff');
+ALTER TABLE salesmans10_40 DROP COLUMN d1;
+ALTER TABLE salesmans10_40 DROP COLUMN d2;
+ALTER TABLE salesmans10_40 DROP COLUMN d3;
+ALTER TABLE salesmans ATTACH PARTITION salesmans10_40 FOR VALUES FROM (10) TO (40);
+INSERT INTO salesmans VALUES (1, 'Poirot');
+INSERT INTO salesmans VALUES (10, 'May');
+INSERT INTO salesmans VALUES (30, 'Ford');
+ALTER TABLE salesmans SPLIT PARTITION salesmans10_40 INTO
+  (PARTITION salesmans10_20 FOR VALUES FROM (10) TO (20),
+   PARTITION salesmans20_30 FOR VALUES FROM (20) TO (30),
+   PARTITION salesmans30_40 FOR VALUES FROM (30) TO (40));
+select * from salesmans01_10;
+ salesman_id | salesman_name 
+-------------+---------------
+           1 | Poirot
+(1 row)
+
+select * from salesmans10_20;
+ salesman_id | salesman_name 
+-------------+---------------
+          19 | Ivanov
+          10 | May
+(2 rows)
+
+select * from salesmans20_30;
+ salesman_id | salesman_name 
+-------------+---------------
+          20 | Smirnoff
+(1 row)
+
+select * from salesmans30_40;
+ salesman_id | salesman_name 
+-------------+---------------
+          30 | Ford
+(1 row)
+
+DROP TABLE salesmans CASCADE;
+--
+-- Test: split sub-partition
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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 (salesman_id INT, salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+           4 | Ivanov        |          750 | 04-13-2022
+(1 row)
+
+SELECT * FROM sales_apr2022_20_30;
+ salesman_id | salesman_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
+(salesman_id INT,
+ salesman_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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Moscow', 'Voronezh', 'Smolensk', 'Bryansk', 'Magadan', 'Kazan', 'Khabarovsk', 'Volgograd', 'Vladivostok');
+CREATE TABLE sales_others PARTITION OF sales_list DEFAULT;
+-- ERROR:  partition "sales_east" would overlap partition "sales_nord"
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk'),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok', 'Murmansk'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd'));
+ERROR:  new partition "sales_east" would overlap with another (not split) partition "sales_nord"
+LINE 3: ...ALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok', 'Murmansk'...
+                                                             ^
+-- 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 ('Voronezh', 'Smolensk', 'Bryansk'),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Voronezh', 'Volgograd'));
+ERROR:  new partition "sales_west" would overlap with another new partition "sales_central"
+LINE 2:   (PARTITION sales_west FOR VALUES IN ('Voronezh', 'Smolensk...
+                                               ^
+-- 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 ('Voronezh', 'Smolensk', 'Bryansk', NULL),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd'));
+ERROR:  new partition "sales_west" cannot have NULL value because split partition does not have
+LINE 2: ...est FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk', NULL),
+                                                                 ^
+DROP TABLE sales_list;
+--
+-- Test: two specific errors for BY LIST partitioning:
+--   * new partitions not has NULL value that split partition has.
+--   * new partitions not has a value that split partition has.
+--
+CREATE TABLE sales_list
+(salesman_id INT,
+ salesman_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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Moscow', 'Voronezh', 'Smolensk', 'Bryansk', 'Magadan', 'Kazan', 'Khabarovsk', 'Volgograd', 'Vladivostok', NULL);
+-- ERROR:  new partitions not have value NULL but split partition has
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk'),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd'));
+ERROR:  new partitions not have value NULL but split partition has
+-- ERROR:  new partitions not have value 'Volgograd' but split partition has
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk'),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', NULL));
+ERROR:  new partitions not have value 'Volgograd' but split partition has
+DROP TABLE sales_list;
+--
+-- Test: BY LIST partitioning, SPLIT PARTITION with data
+--
+CREATE TABLE sales_list
+(salesman_id SERIAL,
+ salesman_name VARCHAR(30),
+ sales_state VARCHAR(20),
+ sales_amount INT,
+ sales_date DATE)
+PARTITION BY LIST (sales_state);
+CREATE INDEX sales_list_salesman_name_idx ON sales_list USING btree (salesman_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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Moscow', 'Voronezh', 'Smolensk', 'Bryansk', 'Magadan', 'Kazan', 'Khabarovsk', 'Volgograd', 'Vladivostok');
+CREATE TABLE sales_others PARTITION OF sales_list DEFAULT;
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Trump',    'Magadan',        1000, '2022-03-01');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Smirnoff', 'Smolensk',       500,  '2022-03-03');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Ford',     'St. Petersburg', 2000, '2022-03-05');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Ivanov',   'Moscow',         750,  '2022-03-04');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Deev',     'Voronezh',       250,  '2022-03-07');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Poirot',   'Kazan',          1000, '2022-03-01');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Ukhta',          1200, '2022-03-06');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Li',       'Vladivostok',    1150, '2022-03-09');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Ukhta',          1200, '2022-03-11');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Halder',   'Murmansk',       800,  '2022-03-02');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Muller',   'Bryansk',        650,  '2022-03-05');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Smith',    'Volgograd',      350,  '2022-03-10');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Gandi',    'Moscow',         150,  '2022-03-08');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Plato',    'Voronezh',       950,  '2022-03-05');
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk'),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd'));
+SELECT * FROM sales_list;
+ salesman_id | salesman_name |  sales_state   | sales_amount | sales_date 
+-------------+---------------+----------------+--------------+------------
+           2 | Smirnoff      | Smolensk       |          500 | 03-03-2022
+           5 | Deev          | Voronezh       |          250 | 03-07-2022
+          11 | Muller        | Bryansk        |          650 | 03-05-2022
+          14 | Plato         | Voronezh       |          950 | 03-05-2022
+           4 | Ivanov        | Moscow         |          750 | 03-04-2022
+           6 | Poirot        | Kazan          |         1000 | 03-01-2022
+          12 | Smith         | Volgograd      |          350 | 03-10-2022
+          13 | Gandi         | Moscow         |          150 | 03-08-2022
+           1 | Trump         | Magadan        |         1000 | 03-01-2022
+           8 | Li            | Vladivostok    |         1150 | 03-09-2022
+           3 | Ford          | St. Petersburg |         2000 | 03-05-2022
+           7 | May           | Ukhta          |         1200 | 03-06-2022
+           9 | May           | Ukhta          |         1200 | 03-11-2022
+          10 | Halder        | Murmansk       |          800 | 03-02-2022
+(14 rows)
+
+SELECT * FROM sales_west;
+ salesman_id | salesman_name | sales_state | sales_amount | sales_date 
+-------------+---------------+-------------+--------------+------------
+           2 | Smirnoff      | Smolensk    |          500 | 03-03-2022
+           5 | Deev          | Voronezh    |          250 | 03-07-2022
+          11 | Muller        | Bryansk     |          650 | 03-05-2022
+          14 | Plato         | Voronezh    |          950 | 03-05-2022
+(4 rows)
+
+SELECT * FROM sales_east;
+ salesman_id | salesman_name | sales_state | sales_amount | sales_date 
+-------------+---------------+-------------+--------------+------------
+           1 | Trump         | Magadan     |         1000 | 03-01-2022
+           8 | Li            | Vladivostok |         1150 | 03-09-2022
+(2 rows)
+
+SELECT * FROM sales_nord;
+ salesman_id | salesman_name |  sales_state   | sales_amount | sales_date 
+-------------+---------------+----------------+--------------+------------
+           3 | Ford          | St. Petersburg |         2000 | 03-05-2022
+           7 | May           | Ukhta          |         1200 | 03-06-2022
+           9 | May           | Ukhta          |         1200 | 03-11-2022
+          10 | Halder        | Murmansk       |          800 | 03-02-2022
+(4 rows)
+
+SELECT * FROM sales_central;
+ salesman_id | salesman_name | sales_state | sales_amount | sales_date 
+-------------+---------------+-------------+--------------+------------
+           4 | Ivanov        | Moscow      |          750 | 03-04-2022
+           6 | Poirot        | Kazan       |         1000 | 03-01-2022
+          12 | Smith         | Volgograd   |          350 | 03-10-2022
+          13 | Gandi         | Moscow      |          150 | 03-08-2022
+(4 rows)
+
+-- Use indexscan for test indexes after split partition
+SET enable_indexscan = ON;
+SET enable_seqscan = OFF;
+SELECT * FROM sales_central WHERE sales_state = 'Moscow';
+ salesman_id | salesman_name | sales_state | sales_amount | sales_date 
+-------------+---------------+-------------+--------------+------------
+           4 | Ivanov        | Moscow      |          750 | 03-04-2022
+          13 | Gandi         | Moscow      |          150 | 03-08-2022
+(2 rows)
+
+SELECT * FROM sales_list WHERE sales_state = 'Moscow';
+ salesman_id | salesman_name | sales_state | sales_amount | sales_date 
+-------------+---------------+-------------+--------------+------------
+           4 | Ivanov        | Moscow      |          750 | 03-04-2022
+          13 | Gandi         | Moscow      |          150 | 03-08-2022
+(2 rows)
+
+SELECT * FROM sales_list WHERE salesman_name = 'Ivanov';
+ salesman_id | salesman_name | sales_state | sales_amount | sales_date 
+-------------+---------------+-------------+--------------+------------
+           4 | Ivanov        | Moscow      |          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 (salesman_id INT, salesman_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;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+          13 | Gandi         |          377 | 01-09-2022
+(1 row)
+
+SELECT * FROM sales_feb2022_1decade;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+           2 | Smirnoff      |          500 | 02-09-2022
+           6 | Poirot        |          150 | 02-07-2022
+(2 rows)
+
+SELECT * FROM sales_mar2022_1decade;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+           7 | Li            |          175 | 03-08-2022
+(1 row)
+
+SELECT * FROM sales_apr2022_1decade;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+           5 | Deev          |          250 | 04-07-2022
+          11 | Trump         |          380 | 04-06-2022
+(2 rows)
+
+SELECT * FROM sales_others;
+ salesman_id | salesman_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 (salesman_id INT, salesman_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;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+          13 | Gandi         |          377 | 01-09-2022
+(1 row)
+
+SELECT * FROM sales_feb2022_1decade;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+           2 | Smirnoff      |          500 | 02-09-2022
+           6 | Poirot        |          150 | 02-07-2022
+(2 rows)
+
+SELECT * FROM sales_mar2022_1decade;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+           7 | Li            |          175 | 03-08-2022
+(1 row)
+
+SELECT * FROM sales_apr2022_1decade;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+           5 | Deev          |          250 | 04-07-2022
+          11 | Trump         |          380 | 04-06-2022
+(2 rows)
+
+SELECT * FROM sales_others;
+ salesman_id | salesman_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 (salesman_id INT, salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+--
+DROP SCHEMA partition_split_schema;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 6f0236d207..117d78cb1b 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..e33e616230
--- /dev/null
+++ b/src/test/regress/sql/partition_split.sql
@@ -0,0 +1,792 @@
+--
+-- PARTITION_SPLIT
+-- Tests for "ALTER TABLE ... SPLIT PARTITION ..." command
+--
+
+CREATE SCHEMA partition_split_schema;
+SET search_path = partition_split_schema, public;
+
+--
+-- BY RANGE partitioning
+--
+
+--
+-- Test for error codes
+--
+CREATE TABLE sales_range (salesman_id int, salesman_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 contains 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" 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" 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" is not equals to upper bound of previous 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-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 equals 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'));
+
+DROP TABLE sales_range;
+DROP TABLE sales_others;
+
+--
+-- Add rows into partitioned table then split partition
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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 (salesman_id INT, salesman_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;
+
+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'));
+
+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 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 (salesman_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(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2021, 12, 7);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2021, 12, 8);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 1, 1);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 2, 4);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 1, 2);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 2, 1);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 3, 3);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 3, 4);
+INSERT INTO sales_date(salesman_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(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 1, 10);
+INSERT INTO sales_date(salesman_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; using a index on partition key; check index after split
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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';
+
+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 test 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';
+SELECT * FROM pg_indexes WHERE tablename = 'sales_mar2022' and schemaname = 'partition_split_schema';
+SELECT * FROM pg_indexes WHERE tablename = 'sales_apr2022' and schemaname = 'partition_split_schema';
+SELECT * FROM pg_indexes WHERE tablename = 'sales_others' and schemaname = 'partition_split_schema';
+
+DROP TABLE sales_range CASCADE;
+
+--
+-- Test: some cases for split DEFAULT partition (different bounds)
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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 (exact 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:  any partition 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 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 (salesman_id INT, salesman_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 equals 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 salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30));
+INSERT INTO salesmans VALUES (1,  'Poirot');
+
+CREATE TABLE sales_range (
+salesman_id INT REFERENCES salesmans(salesman_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;
+
+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;
+SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_mar2022'::regclass::oid;
+SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_apr2022'::regclass::oid;
+
+-- ERROR:  new row for relation "sales_mar2022" violates check constraint "sales_range_salesman_id_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_salesman_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 salesmans CASCADE;
+
+--
+-- Test: split partition on partitioned table in case exists FOREIGN KEY reference from another table
+--
+CREATE TABLE salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+CREATE TABLE sales (salesman_id INT REFERENCES salesmans(salesman_id), sales_amount INT, sales_date DATE);
+
+CREATE TABLE salesmans01_10 PARTITION OF salesmans FOR VALUES FROM (1) TO (10);
+CREATE TABLE salesmans10_40 PARTITION OF salesmans FOR VALUES FROM (10) TO (40);
+
+INSERT INTO salesmans VALUES (1,  'Poirot');
+INSERT INTO salesmans VALUES (10, 'May');
+INSERT INTO salesmans VALUES (19, 'Ivanov');
+INSERT INTO salesmans VALUES (20, 'Smirnoff');
+INSERT INTO salesmans 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 salesmans01_10;
+SELECT * FROM salesmans10_40;
+
+ALTER TABLE salesmans SPLIT PARTITION salesmans10_40 INTO
+  (PARTITION salesmans10_20 FOR VALUES FROM (10) TO (20),
+   PARTITION salesmans20_30 FOR VALUES FROM (20) TO (30),
+   PARTITION salesmans30_40 FOR VALUES FROM (30) TO (40));
+
+SELECT * FROM salesmans01_10;
+SELECT * FROM salesmans10_20;
+SELECT * FROM salesmans20_30;
+SELECT * FROM salesmans30_40;
+
+-- ERROR:  insert or update on table "sales" violates foreign key constraint "sales_salesman_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 salesmans CASCADE;
+
+--
+-- Test: split partition of partitioned table with triggers
+--
+CREATE TABLE salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+
+CREATE TABLE salesmans01_10 PARTITION OF salesmans FOR VALUES FROM (1) TO (10);
+CREATE TABLE salesmans10_40 PARTITION OF salesmans FOR VALUES FROM (10) TO (40);
+
+INSERT INTO salesmans 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 salesmans_after_insert_statement_trigger
+    AFTER INSERT
+    ON salesmans
+    FOR EACH STATEMENT
+    EXECUTE PROCEDURE after_insert_row_trigger('salesmans');
+
+CREATE TRIGGER salesmans_after_insert_row_trigger
+    AFTER INSERT
+    ON salesmans
+    FOR EACH ROW
+    EXECUTE PROCEDURE after_insert_row_trigger('salesmans');
+
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salesmans VALUES (10, 'May');
+-- 1 trigger should fire here (row):
+INSERT INTO salesmans10_40 VALUES (19, 'Ivanov');
+
+ALTER TABLE salesmans SPLIT PARTITION salesmans10_40 INTO
+  (PARTITION salesmans10_20 FOR VALUES FROM (10) TO (20),
+   PARTITION salesmans20_30 FOR VALUES FROM (20) TO (30),
+   PARTITION salesmans30_40 FOR VALUES FROM (30) TO (40));
+
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salesmans VALUES (20, 'Smirnoff');
+-- 1 trigger should fire here (row):
+INSERT INTO salesmans30_40 VALUES (30, 'Ford');
+
+SELECT * FROM salesmans01_10;
+SELECT * FROM salesmans10_20;
+SELECT * FROM salesmans20_30;
+SELECT * FROM salesmans30_40;
+
+DROP TABLE salesmans CASCADE;
+DROP FUNCTION after_insert_row_trigger();
+
+--
+-- Test: split partition with deleted columns
+--
+CREATE TABLE salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+
+CREATE TABLE salesmans01_10 PARTITION OF salesmans FOR VALUES FROM (1) TO (10);
+-- Create new partition with some deleted columns:
+CREATE TABLE salesmans10_40(d1 VARCHAR(30), salesman_id INT PRIMARY KEY, d2 INT, d3 DATE, salesman_name VARCHAR(30));
+
+INSERT INTO salesmans10_40 VALUES ('dummy value 1', 19, 100, now(), 'Ivanov');
+INSERT INTO salesmans10_40 VALUES ('dummy value 2', 20, 101, now(), 'Smirnoff');
+
+ALTER TABLE salesmans10_40 DROP COLUMN d1;
+ALTER TABLE salesmans10_40 DROP COLUMN d2;
+ALTER TABLE salesmans10_40 DROP COLUMN d3;
+
+ALTER TABLE salesmans ATTACH PARTITION salesmans10_40 FOR VALUES FROM (10) TO (40);
+
+INSERT INTO salesmans VALUES (1, 'Poirot');
+INSERT INTO salesmans VALUES (10, 'May');
+INSERT INTO salesmans VALUES (30, 'Ford');
+
+ALTER TABLE salesmans SPLIT PARTITION salesmans10_40 INTO
+  (PARTITION salesmans10_20 FOR VALUES FROM (10) TO (20),
+   PARTITION salesmans20_30 FOR VALUES FROM (20) TO (30),
+   PARTITION salesmans30_40 FOR VALUES FROM (30) TO (40));
+
+select * from salesmans01_10;
+select * from salesmans10_20;
+select * from salesmans20_30;
+select * from salesmans30_40;
+
+DROP TABLE salesmans CASCADE;
+
+--
+-- Test: split sub-partition
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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 (salesman_id INT, salesman_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
+(salesman_id INT,
+ salesman_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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Moscow', 'Voronezh', 'Smolensk', 'Bryansk', 'Magadan', 'Kazan', 'Khabarovsk', 'Volgograd', 'Vladivostok');
+CREATE TABLE sales_others PARTITION OF sales_list DEFAULT;
+
+-- ERROR:  partition "sales_east" would overlap partition "sales_nord"
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk'),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok', 'Murmansk'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd'));
+
+-- 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 ('Voronezh', 'Smolensk', 'Bryansk'),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Voronezh', 'Volgograd'));
+
+-- 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 ('Voronezh', 'Smolensk', 'Bryansk', NULL),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd'));
+
+DROP TABLE sales_list;
+
+--
+-- Test: two specific errors for BY LIST partitioning:
+--   * new partitions not has NULL value that split partition has.
+--   * new partitions not has a value that split partition has.
+--
+CREATE TABLE sales_list
+(salesman_id INT,
+ salesman_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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Moscow', 'Voronezh', 'Smolensk', 'Bryansk', 'Magadan', 'Kazan', 'Khabarovsk', 'Volgograd', 'Vladivostok', NULL);
+
+-- ERROR:  new partitions not have value NULL but split partition has
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk'),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd'));
+
+-- ERROR:  new partitions not have value 'Volgograd' but split partition has
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk'),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', NULL));
+
+DROP TABLE sales_list;
+
+--
+-- Test: BY LIST partitioning, SPLIT PARTITION with data
+--
+CREATE TABLE sales_list
+(salesman_id SERIAL,
+ salesman_name VARCHAR(30),
+ sales_state VARCHAR(20),
+ sales_amount INT,
+ sales_date DATE)
+PARTITION BY LIST (sales_state);
+
+CREATE INDEX sales_list_salesman_name_idx ON sales_list USING btree (salesman_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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Moscow', 'Voronezh', 'Smolensk', 'Bryansk', 'Magadan', 'Kazan', 'Khabarovsk', 'Volgograd', 'Vladivostok');
+CREATE TABLE sales_others PARTITION OF sales_list DEFAULT;
+
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Trump',    'Magadan',        1000, '2022-03-01');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Smirnoff', 'Smolensk',       500,  '2022-03-03');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Ford',     'St. Petersburg', 2000, '2022-03-05');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Ivanov',   'Moscow',         750,  '2022-03-04');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Deev',     'Voronezh',       250,  '2022-03-07');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Poirot',   'Kazan',          1000, '2022-03-01');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Ukhta',          1200, '2022-03-06');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Li',       'Vladivostok',    1150, '2022-03-09');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Ukhta',          1200, '2022-03-11');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Halder',   'Murmansk',       800,  '2022-03-02');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Muller',   'Bryansk',        650,  '2022-03-05');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Smith',    'Volgograd',      350,  '2022-03-10');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Gandi',    'Moscow',         150,  '2022-03-08');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Plato',    'Voronezh',       950,  '2022-03-05');
+
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk'),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd'));
+
+SELECT * FROM sales_list;
+SELECT * FROM sales_west;
+SELECT * FROM sales_east;
+SELECT * FROM sales_nord;
+SELECT * FROM sales_central;
+
+-- Use indexscan for test indexes after split partition
+SET enable_indexscan = ON;
+SET enable_seqscan = OFF;
+
+SELECT * FROM sales_central WHERE sales_state = 'Moscow';
+SELECT * FROM sales_list WHERE sales_state = 'Moscow';
+SELECT * FROM sales_list WHERE salesman_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 (salesman_id INT, salesman_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 (salesman_id INT, salesman_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 (salesman_id INT, salesman_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;
+
+--
+DROP SCHEMA partition_split_schema;
-- 
2.40.1.windows.1


From 383fdf8857a9d36bd5350c06d0dfbbe02499504c Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email protected]>
Date: Tue, 28 Mar 2023 10:50:19 +0300
Subject: [PATCH v23 3/4] Documentation for ALTER TABLE SPLIT PARTITION/MERGE
 PARTITIONS commands

---
 doc/src/sgml/ref/alter_table.sgml | 124 +++++++++++++++++++++++++++++-
 1 file changed, 121 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 9670671107..9fb31df250 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -37,6 +37,13 @@ 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>
 
 <phrase>where <replaceable class="parameter">action</replaceable> is one of:</phrase>
 
@@ -1106,14 +1113,99 @@ 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 split 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 DEFAULT partition, one of new partitions must be DEFAULT.
+      In case one of 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 contains 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>
+    </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 merge several partitions into 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 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 values lists of all partitions
+         <replaceable class="parameter">partition_name1</replaceable>,
+         <replaceable class="parameter">partition_name2</replaceable> [, ...] are
+         combined and form a 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>
+    </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>,
+   <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
@@ -1356,7 +1448,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 split partition, or the name of the new merged partition.
        </para>
       </listitem>
      </varlistentry>
@@ -1772,6 +1865,31 @@ 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 ('Voronezh', 'Smolensk', 'Bryansk'),
+    PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+    PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd'));
+</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>
-- 
2.40.1.windows.1


From d1dd3629e2817ee4d60ce12a0fde5ea9bdaecee0 Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email protected]>
Date: Thu, 26 Oct 2023 03:35:24 +0300
Subject: [PATCH v23 4/4] SPLIT PARTITION optimization

---
 src/backend/commands/tablecmds.c              | 686 +++++++++++++-----
 src/test/regress/expected/partition_split.out | 307 ++++++++
 src/test/regress/sql/partition_split.sql      | 152 ++++
 3 files changed, 978 insertions(+), 167 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b0d2e4af91..52e8ad2b35 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -20654,20 +20654,47 @@ typedef struct SplitPartitionContext
 	BulkInsertState bistate;	/* state of bulk inserts for partition */
 	TupleTableSlot *dstslot;	/* slot for insert row into partition */
 	Relation	partRel;		/* relation for partition */
+	SinglePartitionSpec *sps;	/* info about single partition (from SQL
+								 * command) */
 }			SplitPartitionContext;
 
+/*
+ * Struct with context of SPLIT PARTITION operation
+ */
+typedef struct SplitInfo
+{
+	PartitionCmd *cmd;			/* SPLIT PARTITION command info */
+
+	Relation	rel;			/* partitioned table */
+	Relation	splitRel;		/* split partition */
+
+	Oid			defaultPartOid; /* identifier of DEFAULT-partition in rel (if
+								 * exists) */
+	List	   *partContexts;	/* list of structs SplitPartitionContext (each
+								 * struct for each new partition) */
+	SplitPartitionContext *defaultPartCtx;	/* pointer to DEFAULT-partition in
+											 * partContexts list (if exists) */
+	EState	   *estate;			/* working state */
+}			SplitInfo;
 
 /*
- * createSplitPartitionContext: create context for partition and fill it
+ * createSplitPartitionContext: create context for partition
  */
 static SplitPartitionContext *
-createSplitPartitionContext(Relation partRel)
+createSplitPartitionContext(SinglePartitionSpec * sps)
 {
-	SplitPartitionContext *pc;
+	SplitPartitionContext *pc = (SplitPartitionContext *) palloc0(sizeof(SplitPartitionContext));
 
-	pc = (SplitPartitionContext *) palloc0(sizeof(SplitPartitionContext));
-	pc->partRel = partRel;
+	pc->sps = sps;
+	return pc;
+}
 
+/*
+ * fillSplitPartitionContext: fill partition context
+ */
+static void
+fillSplitPartitionContext(SplitPartitionContext * pc)
+{
 	/*
 	 * Prepare a BulkInsertState for table_tuple_insert. The FSM is empty, so
 	 * don't bother using it.
@@ -20678,67 +20705,66 @@ createSplitPartitionContext(Relation partRel)
 	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)
+deleteSplitPartitionContext(SplitPartitionContext * pc)
 {
-	ExecDropSingleTupleTableSlot(pc->dstslot);
-	FreeBulkInsertState(pc->bistate);
+	if (pc->dstslot)
+		ExecDropSingleTupleTableSlot(pc->dstslot);
+
+	if (pc->bistate)
+	{
+		/* The FSM is empty, so don't bother using it. */
+		int			ti_options = TABLE_INSERT_SKIP_FSM;
 
-	table_finish_bulk_insert(pc->partRel, ti_options);
+		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.
+ * createSplitInfo: create SPLIT PARTITION command context, contexts for new
+ *					partitions and generate constraints for them.
+ *					We need to use constraints for optimization.
  *
- * New partitions description:
- * partlist: list of pointers to SinglePartitionSpec structures.
- * newPartRels: list of Relation's.
+ * cmd: SPLIT PARTITION command info.
+ * rel: partitioned table.
+ * splitRel: split partition.
  * defaultPartOid: oid of DEFAULT partition, for table rel.
  */
-static void
-moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPartRels, Oid defaultPartOid)
+static SplitInfo *
+createSplitInfo(PartitionCmd *cmd, Relation rel, Relation splitRel,
+				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;
+	SplitInfo  *si;
+	ListCell   *listptr;
 
-	mycid = GetCurrentCommandId(true);
+	si = (SplitInfo *) palloc0(sizeof(SplitInfo));
 
-	estate = CreateExecutorState();
+	si->cmd = cmd;
+	si->rel = rel;
+	si->splitRel = splitRel;
 
-	forboth(listptr, partlist, listptr2, newPartRels)
+	si->defaultPartOid = defaultPartOid;
+	si->estate = CreateExecutorState();
+
+	/* Create context for each new partition and fill it. */
+	foreach(listptr, cmd->partlist)
 	{
 		SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
-
-		pc = createSplitPartitionContext((Relation) lfirst(listptr2));
+		SplitPartitionContext *pc = createSplitPartitionContext(sps);
 
 		if (sps->bound->is_default)
 		{
 			/* We should not create constraint for detached DEFAULT partition. */
-			defaultPartCtx = pc;
+			si->defaultPartCtx = pc;
 		}
 		else
 		{
@@ -20746,9 +20772,8 @@ moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPar
 
 			/* Build expression execution states for partition check quals. */
 			partConstraint = get_qual_from_partbound(rel, sps->bound);
-			partConstraint =
-				(List *) eval_const_expressions(NULL,
-												(Node *) partConstraint);
+			partConstraint = (List *) eval_const_expressions(NULL, (Node *) partConstraint);
+
 			/* Make boolean expression for ExecCheck(). */
 			partConstraint = list_make1(make_ands_explicit(partConstraint));
 
@@ -20756,11 +20781,10 @@ moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPar
 			 * Map the vars in the constraint expression from rel's attnos to
 			 * splitRel's.
 			 */
-			partConstraint = map_partition_varattnos(partConstraint,
-													 1, splitRel, rel);
+			partConstraint = map_partition_varattnos(partConstraint, 1, splitRel, rel);
 
 			pc->partqualstate =
-				ExecPrepareExpr((Expr *) linitial(partConstraint), estate);
+				ExecPrepareExpr((Expr *) linitial(partConstraint), si->estate);
 			Assert(pc->partqualstate != NULL);
 		}
 
@@ -20768,41 +20792,261 @@ moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPar
 		partContexts = lappend(partContexts, pc);
 	}
 
+	si->partContexts = partContexts;
+
+	return si;
+}
+
+/*
+ * deleteSplitInfo: delete SPLIT PARTITION command context
+ */
+static void
+deleteSplitInfo(SplitInfo * si)
+{
+	ListCell   *listptr;
+
+	FreeExecutorState(si->estate);
+
+	foreach(listptr, si->partContexts)
+		deleteSplitPartitionContext((SplitPartitionContext *) lfirst(listptr));
+
+	pfree(si);
+}
+
+/*
+ * checkNewPartitions: simple check of the new partitions.
+ *
+ * cmd: SPLIT PARTITION command info.
+ * splitRelOid: split partition Oid.
+ *
+ * Returns true if one of the new partitions has the same name as the split
+ * partition.
+ */
+static bool
+checkNewPartitions(PartitionCmd *cmd, Oid splitRelOid)
+{
+	Oid			namespaceId;
+	ListCell   *listptr;
+	bool		isSameName = false;
+	char		relname[NAMEDATALEN];
+
+	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.
+		 */
+		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)));
+	}
+
+	return isSameName;
+}
+
+/*
+ * createPartitionTable: create table for new partition with given name
+ * (newPartName) like table (modelRelName)
+ *
+ * Emulates command: CREATE TABLE <newPartName> (LIKE <modelRelName>
+ * INCLUDING ALL EXCLUDING INDEXES)
+ */
+static void
+createPartitionTable(RangeVar *newPartName, RangeVar *modelRelName,
+					 AlterTableUtilityContext *context)
+{
+	CreateStmt *createStmt;
+	TableLikeClause *tlc;
+	PlannedStmt *wrapper;
+
+	createStmt = makeNode(CreateStmt);
+	createStmt->relation = newPartName;
+	createStmt->tableElts = NIL;
+	createStmt->inhRelations = NIL;
+	createStmt->constraints = NIL;
+	createStmt->options = NIL;
+	createStmt->oncommit = ONCOMMIT_NOOP;
+	createStmt->tablespacename = NULL;
+	createStmt->if_not_exists = false;
+
+	tlc = makeNode(TableLikeClause);
+	tlc->relation = modelRelName;
+
+	/*
+	 * Indexes will be inherited on "attach new partitions" stage, after data
+	 * moving.
+	 */
+	tlc->options = CREATE_TABLE_LIKE_ALL & ~CREATE_TABLE_LIKE_INDEXES;
+	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);
+}
+
+/*
+ * createNewPartitions: simple check of the new partitions.
+ *
+ * si: SPLIT PARTITION command context.
+ * splitName: split partition name.
+ * pcWithAllRows: context of partition that contains all the rows of the split
+ * 				  partition or NULL if no such partition exists.
+ *
+ * Function returns name of split partition (and can change it in case of
+ * optimization with split partition renaming).
+ */
+static RangeVar *
+createNewPartitions(SplitInfo * si, RangeVar *splitName,
+					SplitPartitionContext * pcWithAllRows,
+					AlterTableUtilityContext *context)
+{
+	ListCell   *listptr;
+	Oid			splitRelOid;
+	RangeVar   *splitPartName = splitName;
+
+	splitRelOid = RelationGetRelid(si->splitRel);
+
+	foreach(listptr, si->partContexts)
+	{
+		SplitPartitionContext *pc = (SplitPartitionContext *) lfirst(listptr);
+
+		if (pc == pcWithAllRows)
+		{
+			/* Need to reuse splitRel for partition instead of creation. */
+
+			/*
+			 * We must bump the command counter to make the split partition
+			 * tuple visible for rename.
+			 */
+			CommandCounterIncrement();
+
+			/*
+			 * Rename split partition to new partition.
+			 */
+			RenameRelationInternal(splitRelOid, pc->sps->name->relname, false, false);
+			splitPartName = makeRangeVar(get_namespace_name(RelationGetNamespace(si->splitRel)),
+										 pc->sps->name->relname, -1);
+
+			/*
+			 * We must bump the command counter to make the split partition
+			 * tuple visible after rename.
+			 */
+			CommandCounterIncrement();
+
+			pc->partRel = si->splitRel;
+			/* No need to open relation : splitRel is already opened. */
+		}
+		else
+		{
+			createPartitionTable(pc->sps->name, splitPartName, context);
+
+			/* Open the new partition and acquire exclusive lock on it. */
+			pc->partRel = table_openrv(pc->sps->name, AccessExclusiveLock);
+		}
+	}
+
+	return splitPartName;
+}
+
+/*
+ * moveSplitTableRows: scan split partition (splitRel) of partitioned table
+ * (rel) and move rows into new partitions.
+ *
+ * si: SPLIT PARTITION command context.
+ */
+static void
+moveSplitTableRows(SplitInfo * si)
+{
+	/* The FSM is empty, so don't bother using it. */
+	int			ti_options = TABLE_INSERT_SKIP_FSM;
+	CommandId	mycid;
+	ListCell   *listptr;
+	TupleTableSlot *srcslot;
+	ExprContext *econtext;
+	TableScanDesc scan;
+	Snapshot	snapshot;
+	MemoryContext oldCxt;
+	TupleConversionMap *tuple_map;
+	SplitPartitionContext *pc = NULL;
+	bool		isOldDefaultPart = false;
+	SplitPartitionContext *defaultPartCtx = si->defaultPartCtx;
+
+	mycid = GetCurrentCommandId(true);
+
+	/* Prepare new partitions contexts for insert rows. */
+	foreach(listptr, si->partContexts)
+		fillSplitPartitionContext((SplitPartitionContext *) lfirst(listptr));
+
 	/*
 	 * 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))
+	if (!defaultPartCtx && OidIsValid(si->defaultPartOid))
 	{
 		/* Indicate that we allocate context for old DEFAULT partition */
 		isOldDefaultPart = true;
-		defaultPartCtx = createSplitPartitionContext(table_open(defaultPartOid, AccessExclusiveLock));
+		defaultPartCtx = createSplitPartitionContext(NULL);
+		defaultPartCtx->partRel = table_open(si->defaultPartOid, AccessExclusiveLock);
+		fillSplitPartitionContext(defaultPartCtx);
 	}
 
-	econtext = GetPerTupleExprContext(estate);
+	econtext = GetPerTupleExprContext(si->estate);
 
 	/* Create necessary tuple slot. */
-	srcslot = MakeSingleTupleTableSlot(RelationGetDescr(splitRel),
-									   table_slot_callbacks(splitRel));
+	srcslot = MakeSingleTupleTableSlot(RelationGetDescr(si->splitRel),
+									   table_slot_callbacks(si->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),
+	pc = (SplitPartitionContext *) lfirst(list_head(si->partContexts));
+	tuple_map = convert_tuples_by_name(RelationGetDescr(si->splitRel),
 									   RelationGetDescr(pc->partRel));
 
 	/* Scan through the rows. */
 	snapshot = RegisterSnapshot(GetLatestSnapshot());
-	scan = table_beginscan(splitRel, snapshot, 0, NULL);
+	scan = table_beginscan(si->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));
+	oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(si->estate));
 
 	while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot))
 	{
@@ -20815,7 +21059,7 @@ moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPar
 		econtext->ecxt_scantuple = srcslot;
 
 		/* Search partition for current slot srcslot. */
-		foreach(listptr, partContexts)
+		foreach(listptr, si->partContexts)
 		{
 			pc = (SplitPartitionContext *) lfirst(listptr);
 
@@ -20836,7 +21080,7 @@ moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPar
 				ereport(ERROR,
 						(errcode(ERRCODE_CHECK_VIOLATION),
 						 errmsg("can not find partition for split partition row"),
-						 errtable(splitRel)));
+						 errtable(si->splitRel)));
 		}
 
 		if (tuple_map)
@@ -20877,74 +21121,208 @@ moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPar
 
 	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;
+		Relation	defaultPartRel = defaultPartCtx->partRel;
 
-		deleteSplitPartitionContext(defaultPartCtx, ti_options);
+		deleteSplitPartitionContext(defaultPartCtx);
 		/* Keep the lock until commit. */
 		table_close(defaultPartRel, NoLock);
 	}
 }
 
 /*
- * createPartitionTable: create table for new partition with given name
- * (newPartName) like table (modelRelName)
+ * findNewPartForSlot: find partition that contains slot value.
  *
- * Emulates command: CREATE TABLE <newPartName> (LIKE <modelRelName>
- * INCLUDING ALL EXCLUDING INDEXES)
+ * si: SPLIT PARTITION context.
+ * checkPc: partition context for check slot value (can be NULL).
+ * slot: value to check.
  */
-static void
-createPartitionTable(RangeVar *newPartName, RangeVar *modelRelName,
-					 AlterTableUtilityContext *context)
+static SplitPartitionContext *
+findNewPartForSlot(SplitInfo * si, SplitPartitionContext * checkPc, TupleTableSlot *slot)
 {
-	CreateStmt *createStmt;
-	TableLikeClause *tlc;
-	PlannedStmt *wrapper;
+	ListCell   *listptr;
+	ExprContext *econtext;
+	MemoryContext oldCxt;
+	SplitPartitionContext *result = NULL;
 
-	createStmt = makeNode(CreateStmt);
-	createStmt->relation = newPartName;
-	createStmt->tableElts = NIL;
-	createStmt->inhRelations = NIL;
-	createStmt->constraints = NIL;
-	createStmt->options = NIL;
-	createStmt->oncommit = ONCOMMIT_NOOP;
-	createStmt->tablespacename = NULL;
-	createStmt->if_not_exists = false;
+	econtext = GetPerTupleExprContext(si->estate);
 
-	tlc = makeNode(TableLikeClause);
-	tlc->relation = modelRelName;
+	/* Make sure the tuple is fully deconstructed. */
+	slot_getallattrs(slot);
+
+	econtext->ecxt_scantuple = slot;
 
 	/*
-	 * Indexes will be inherited on "attach new partitions" stage, after data
-	 * moving.
+	 * Switch to per-tuple memory context and reset it after each check, so we
+	 * don't leak memory.
 	 */
-	tlc->options = CREATE_TABLE_LIKE_ALL & ~CREATE_TABLE_LIKE_INDEXES;
-	tlc->relationOid = InvalidOid;
-	createStmt->tableElts = lappend(createStmt->tableElts, tlc);
+	oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(si->estate));
 
-	/* 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;
+	if (checkPc)
+	{
+		if (ExecCheck(checkPc->partqualstate, econtext))
+		{
+			ResetExprContext(econtext);
+			result = checkPc;
+		}
+	}
+	else
+	{
+		/* Search partition for current slot srcslot. */
+		foreach(listptr, si->partContexts)
+		{
+			SplitPartitionContext *pc = (SplitPartitionContext *) lfirst(listptr);
 
-	ProcessUtility(wrapper,
-				   context->queryString,
-				   false,
-				   PROCESS_UTILITY_SUBCOMMAND,
-				   NULL,
-				   NULL,
-				   None_Receiver,
-				   NULL);
+			if (pc->partqualstate /* skip DEFAULT partition */ &&
+				ExecCheck(pc->partqualstate, econtext))
+			{
+				ResetExprContext(econtext);
+				result = pc;
+				break;
+			}
+			ResetExprContext(econtext);
+		}
+
+		/* We not found partition with borders but exists DEFAULT partition. */
+		if (!result && si->defaultPartCtx)
+			result = si->defaultPartCtx;
+
+		/*
+		 * "result" can be NULL here because can be spaces between of the new
+		 * partitions and rows from the spaces can be moved to the DEFAULT
+		 * partition of the partitioned table.
+		 */
+	}
+
+	MemoryContextSwitchTo(oldCxt);
+
+	return result;
+}
+
+/*
+ * findNewPartWithAllRows: find partition that contains all the rows of the
+ * split partition; returns partition context if partition was found.
+ *
+ * si: SPLIT PARTITION context.
+ */
+static SplitPartitionContext *
+findNewPartWithAllRows(SplitInfo * si)
+{
+	PartitionKey key = RelationGetPartitionKey(si->rel);
+	ListCell   *index;
+	int			partnatts;
+	SplitPartitionContext *result = NULL;
+	AttrMap    *map;
+	AttrNumber *partattrs;
+	int			i;
+
+	/* We can use optimization for BY RANGE partitioning only. */
+	if (key->strategy != PARTITION_STRATEGY_RANGE)
+		return NULL;
+
+	partnatts = get_partition_natts(key);
+
+	/*
+	 * Partition key contains columns of partitioned tables si->rel but index
+	 * contains columns of si->splitRel. So we need a map for convert
+	 * attributes numbers (si->rel) -> (si->splitRel).
+	 */
+	map = build_attrmap_by_name_if_req(RelationGetDescr(si->splitRel),
+									   RelationGetDescr(si->rel),
+									   false);
+	if (map)
+	{
+		/*
+		 * Columns order in a partitioned table and split partition is
+		 * different. So need to create a new array with attribute numbers.
+		 */
+		partattrs = palloc(sizeof(AttrNumber) * partnatts);
+		for (i = 0; i < partnatts; i++)
+		{
+			AttrNumber	attr_num = get_partition_col_attnum(key, i);
+
+			partattrs[i] = map->attnums[attr_num - 1];
+		}
+	}
+	else
+	{
+		/* We can use array of partition key. */
+		partattrs = key->partattrs;
+	}
+
+	/* Scan all indexes of split partition. */
+	foreach(index, RelationGetIndexList(si->splitRel))
+	{
+		Oid			thisIndexOid = lfirst_oid(index);
+		Relation	indexRel = index_open(thisIndexOid, AccessShareLock);
+
+		/*
+		 * Index should be valid, btree (for searching min/max) and contain
+		 * the same columns as partition key.
+		 */
+		if (indexRel->rd_index->indisvalid &&
+			indexRel->rd_rel->relam == BTREE_AM_OID &&
+			indexRel->rd_index->indnatts == partnatts)
+		{
+			for (i = 0; i < indexRel->rd_index->indnatts; i++)
+			{
+				if (indexRel->rd_index->indkey.values[i] != partattrs[i])
+					break;
+			}
+
+			/* Index found? */
+			if (i == indexRel->rd_index->indnatts)
+			{
+				IndexScanDesc indexScan;
+				TupleTableSlot *slot;
+
+				indexScan = index_beginscan(si->splitRel, indexRel, SnapshotAny, 0, 0);
+				do
+				{
+					SplitPartitionContext *pc;
+
+					/* Search a minimum index value. */
+					index_rescan(indexScan, NULL, 0, NULL, 0);
+					slot = table_slot_create(si->splitRel, NULL);
+					if (!index_getnext_slot(indexScan, ForwardScanDirection, slot))
+					{
+						ExecDropSingleTupleTableSlot(slot);
+						break;
+					}
+					/* Find partition context for minimum index value. */
+					pc = findNewPartForSlot(si, NULL, slot);
+					ExecDropSingleTupleTableSlot(slot);
+
+					/* Search a maximum index value. */
+					index_rescan(indexScan, NULL, 0, NULL, 0);
+					slot = table_slot_create(si->splitRel, NULL);
+					if (!index_getnext_slot(indexScan, BackwardScanDirection, slot))
+					{
+						ExecDropSingleTupleTableSlot(slot);
+						break;
+					}
+					/* Check partition context "pc" for maximum index value. */
+					result = findNewPartForSlot(si, pc, slot);
+					ExecDropSingleTupleTableSlot(slot);
+				} while (0);
+
+				index_endscan(indexScan);
+				index_close(indexRel, AccessShareLock);
+				goto done;
+			}
+		}
+		index_close(indexRel, AccessShareLock);
+	}
+
+done:
+	if (map)
+	{
+		pfree(partattrs);
+		free_attrmap(map);
+	}
+	return result;
 }
 
 /*
@@ -20956,16 +21334,14 @@ ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 {
 	Relation	splitRel;
 	Oid			splitRelOid;
-	char		relname[NAMEDATALEN];
-	Oid			namespaceId;
-	ListCell   *listptr,
-			   *listptr2;
+	ListCell   *listptr;
 	bool		isSameName = false;
 	char		tmpRelName[NAMEDATALEN];
-	List	   *newPartRels = NIL;
 	ObjectAddress object;
 	RangeVar   *splitPartName = cmd->name;
 	Oid			defaultPartOid;
+	SplitPartitionContext *pcWithAllRows;
+	SplitInfo  *si;
 
 	defaultPartOid = get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true));
 
@@ -20984,35 +21360,7 @@ ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	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.
-		 */
-		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)));
-	}
+	isSameName = checkNewPartitions(cmd, splitRelOid);
 
 	/* Detach split partition. */
 	RemoveInheritance(splitRel, rel, false);
@@ -21033,8 +21381,7 @@ ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		/* Rename partition. */
 		sprintf(tmpRelName, "split-%u-%X-tmp", RelationGetRelid(rel), MyProcPid);
 		RenameRelationInternal(splitRelOid, tmpRelName, false, false);
-		splitPartName = makeRangeVar(
-									 get_namespace_name(RelationGetNamespace(splitRel)),
+		splitPartName = makeRangeVar(get_namespace_name(RelationGetNamespace(splitRel)),
 									 tmpRelName, -1);
 
 		/*
@@ -21044,43 +21391,48 @@ ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		CommandCounterIncrement();
 	}
 
-	/* Create new partitions (like split partition), without indexes. */
-	foreach(listptr, cmd->partlist)
-	{
-		SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
-		Relation	newPartRel;
+	/* Create SPLIT PARTITION context. */
+	si = createSplitInfo(cmd, rel, splitRel, defaultPartOid);
 
-		createPartitionTable(sps->name, splitPartName, context);
+	/*
+	 * Optimization: if exist a new partition that contains all the rows of
+	 * the split partition then do not copy rows, rename the split partition.
+	 */
+	pcWithAllRows = findNewPartWithAllRows(si);
 
-		/* Open the new partition and acquire exclusive lock on it. */
-		newPartRel = table_openrv(sps->name, AccessExclusiveLock);
+	/* Create new partitions (like split partition), without indexes. */
+	splitPartName = createNewPartitions(si, splitPartName, pcWithAllRows, context);
 
-		newPartRels = lappend(newPartRels, newPartRel);
+	if (!pcWithAllRows)
+	{
+		/* Copy data from split partition to new partitions. */
+		moveSplitTableRows(si);
+		/* Keep the lock until commit. */
+		table_close(splitRel, NoLock);
 	}
 
-	/* 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)
+	foreach(listptr, si->partContexts)
 	{
-		SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
-		Relation	newPartRel = (Relation) lfirst(listptr2);
+		SplitPartitionContext *pc = (SplitPartitionContext *) lfirst(listptr);
 
 		/* wqueue = NULL: verification for each cloned constraint is not need. */
-		attachPartitionTable(NULL, rel, newPartRel, sps->bound);
+		attachPartitionTable(NULL, rel, pc->partRel, pc->sps->bound);
 		/* Keep the lock until commit. */
-		table_close(newPartRel, NoLock);
+		table_close(pc->partRel, NoLock);
 	}
 
-	/* Drop split partition. */
-	object.classId = RelationRelationId;
-	object.objectId = splitRelOid;
-	object.objectSubId = 0;
-	/* Probably DROP_CASCADE is not needed. */
-	performDeletion(&object, DROP_RESTRICT, 0);
+	if (!pcWithAllRows)
+	{
+		/* Drop split partition. */
+		object.classId = RelationRelationId;
+		object.objectId = splitRelOid;
+		object.objectSubId = 0;
+		/* Probably DROP_CASCADE is not needed. */
+		performDeletion(&object, DROP_RESTRICT, 0);
+	}
+
+	deleteSplitInfo(si);
 }
 
 /*
@@ -21226,8 +21578,8 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
 							RelationGetRelationName(pc->partRel))));
 
 		/*
-		 * Checking that two partitions have the same name was before,
-		 * in function transformPartitionCmdForMerge().
+		 * Checking that two partitions have the same name was before, in
+		 * function transformPartitionCmdForMerge().
 		 */
 		if (equal(name, cmd->name))
 			/* One new partition can have the same name as merged partition. */
diff --git a/src/test/regress/expected/partition_split.out b/src/test/regress/expected/partition_split.out
index 38736d3e67..4627a91e57 100644
--- a/src/test/regress/expected/partition_split.out
+++ b/src/test/regress/expected/partition_split.out
@@ -1314,4 +1314,311 @@ SELECT * FROM sales_others;
 
 DROP TABLE sales_range;
 --
+--
+-- Tests for SPLIT optimization (BY RANGE partitioning): if one of the new
+-- partitions contains all the rows of the split partition, then we can rename
+-- the split partition instead of creating a new partition and moving the rows.
+--
+-- 1. Optimization should be used.
+--
+CREATE TABLE test(name text, i int) PARTITION BY RANGE (i);
+CREATE TABLE test_1 PARTITION OF test FOR VALUES FROM (1) TO (10);
+CREATE TABLE test_def PARTITION OF test DEFAULT;
+CREATE INDEX idx_test_i ON test(i);
+INSERT INTO test(name, i) VALUES
+  ('a1', 1), ('a5', 5), ('a15', 15), ('a12', 12), ('a17', 17), ('a11', 11);
+-- should be rows 15, 12, 17, 11:
+SELECT i FROM test_def;
+ i  
+----
+ 15
+ 12
+ 17
+ 11
+(4 rows)
+
+SELECT 'test_def'::regclass::oid AS prev_oid \gset
+ALTER TABLE test SPLIT PARTITION test_def INTO 
+  (PARTITION test_def DEFAULT, PARTITION test_2 FOR VALUES FROM (11) TO (19));
+-- should be 't' (table "test_2" after SPLIT should be the same as table
+-- "test_def" before SPLIT):
+SELECT 'test_2'::regclass::oid=:prev_oid;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- should be rows 15, 12, 17, 11:
+SELECT i FROM test_2;
+ i  
+----
+ 15
+ 12
+ 17
+ 11
+(4 rows)
+
+-- should be 0 rows:
+SELECT i FROM test_def;
+ i 
+---
+(0 rows)
+
+-- should be 6 rows:
+SELECT * FROM test;
+ name | i  
+------+----
+ a1   |  1
+ a5   |  5
+ a15  | 15
+ a12  | 12
+ a17  | 17
+ a11  | 11
+(6 rows)
+
+DROP TABLE test CASCADE;
+--
+-- 2. Optimization cannot be used because not exists btree-index on the
+-- partition key (it is used to check the placement of rows in the partitions).
+--
+CREATE TABLE test(name text, i int) PARTITION BY RANGE (i);
+CREATE TABLE test_1 PARTITION OF test FOR VALUES FROM (1) TO (10);
+CREATE TABLE test_def PARTITION OF test DEFAULT;
+INSERT INTO test(name, i) VALUES
+  ('a1', 1), ('a5', 5), ('a15', 15), ('a12', 12), ('a17', 17), ('a11', 11);
+SELECT 'test_def'::regclass::oid AS prev_oid \gset
+ALTER TABLE test SPLIT PARTITION test_def INTO 
+  (PARTITION test_def DEFAULT, PARTITION test_2 FOR VALUES FROM (11) TO (19));
+-- should be 'f' (tables "test_2" and "test_def" should be different):
+SELECT 'test_2'::regclass::oid=:prev_oid;
+ ?column? 
+----------
+ f
+(1 row)
+
+-- should be rows 15, 12, 17, 11:
+SELECT i FROM test_2;
+ i  
+----
+ 15
+ 12
+ 17
+ 11
+(4 rows)
+
+-- should be 6 rows:
+SELECT * FROM test;
+ name | i  
+------+----
+ a1   |  1
+ a5   |  5
+ a15  | 15
+ a12  | 12
+ a17  | 17
+ a11  | 11
+(6 rows)
+
+DROP TABLE test CASCADE;
+--
+-- 3. Optimization cannot be used because rows should be moved into different
+-- partitions.
+--
+CREATE TABLE test(name text, i int) PARTITION BY RANGE (i);
+CREATE TABLE test_1 PARTITION OF test FOR VALUES FROM (1) TO (10);
+CREATE TABLE test_def PARTITION OF test DEFAULT;
+CREATE INDEX idx_test_i ON test(i);
+INSERT INTO test(name, i) VALUES
+  ('a1', 1), ('a5', 5), ('a15', 15), ('a12', 12), ('a27', 27), ('a11', 11);
+SELECT 'test_def'::regclass::oid AS prev_oid \gset
+ALTER TABLE test SPLIT PARTITION test_def INTO 
+  (PARTITION test_def DEFAULT, PARTITION test_2 FOR VALUES FROM (11) TO (19));
+-- should be 'f' (tables "test_2" and "test_def" should be different):
+SELECT 'test_2'::regclass::oid=:prev_oid;
+ ?column? 
+----------
+ f
+(1 row)
+
+-- should be rows 15, 12, 11:
+SELECT i FROM test_2;
+ i  
+----
+ 15
+ 12
+ 11
+(3 rows)
+
+-- should be 6 rows:
+SELECT * FROM test;
+ name | i  
+------+----
+ a1   |  1
+ a5   |  5
+ a15  | 15
+ a12  | 12
+ a11  | 11
+ a27  | 27
+(6 rows)
+
+DROP TABLE test CASCADE;
+--
+-- 4. Optimization should be used, DEFAUT partition renames to DEFAULT
+-- partition.
+--
+CREATE TABLE test(name text, i int) PARTITION BY RANGE (i);
+CREATE TABLE test_1 PARTITION OF test FOR VALUES FROM (1) TO (10);
+CREATE TABLE test_def PARTITION OF test DEFAULT;
+CREATE INDEX idx_test_i ON test(i);
+INSERT INTO test(name, i) VALUES
+  ('a1', 1), ('a5', 5), ('a25', 25), ('a22', 22), ('a27', 27), ('a21', 21);
+-- should be rows 25, 22, 27, 21:
+SELECT i FROM test_def;
+ i  
+----
+ 25
+ 22
+ 27
+ 21
+(4 rows)
+
+SELECT 'test_def'::regclass::oid AS prev_oid \gset
+ALTER TABLE test SPLIT PARTITION test_def INTO 
+  (PARTITION test_def DEFAULT, PARTITION test_2 FOR VALUES FROM (11) TO (19));
+-- should be 't' (table "test_def" after SPLIT should be the same as table
+-- "test_def" before SPLIT):
+SELECT 'test_def'::regclass::oid=:prev_oid;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- should be 0 rows:
+SELECT i FROM test_2;
+ i 
+---
+(0 rows)
+
+-- should be rows 25, 22, 27, 21:
+SELECT i FROM test_def;
+ i  
+----
+ 25
+ 22
+ 27
+ 21
+(4 rows)
+
+-- should be 6 rows:
+SELECT * FROM test;
+ name | i  
+------+----
+ a1   |  1
+ a5   |  5
+ a25  | 25
+ a22  | 22
+ a27  | 27
+ a21  | 21
+(6 rows)
+
+DROP TABLE test CASCADE;
+--
+-- 5. Optimization should be used, 2-column partition key + different columns
+-- order in partitions.
+--
+CREATE TABLE test_2colkey(s smallint, b bigint, t text) PARTITION BY RANGE (b, s);
+CREATE TABLE test_2colkey_1 PARTITION OF test_2colkey FOR VALUES FROM (1000000001, 1) TO (1000000100, 100);
+CREATE TABLE test_2colkey_def(i int, b bigint, s smallint, t text);
+ALTER TABLE test_2colkey_def DROP COLUMN i;
+ALTER TABLE test_2colkey ATTACH PARTITION test_2colkey_def DEFAULT;
+CREATE INDEX idx_test_2colkey_s_b ON test_2colkey(b, s);
+INSERT INTO test_2colkey (b, s, t) VALUES (1000000010, 3, 'value_10_3');
+INSERT INTO test_2colkey (b, s, t) VALUES (1000000120, 4, 'value_120_4');
+INSERT INTO test_2colkey (b, s, t) VALUES (1000000003, 5, 'value_3_5');
+INSERT INTO test_2colkey (b, s, t) VALUES (1000000124, 2, 'value_124_2');
+-- should be 4 rows:
+SELECT b, s FROM test_2colkey;
+     b      | s 
+------------+---
+ 1000000010 | 3
+ 1000000003 | 5
+ 1000000120 | 4
+ 1000000124 | 2
+(4 rows)
+
+-- should be 2 rows:
+SELECT b, s FROM test_2colkey_def;
+     b      | s 
+------------+---
+ 1000000120 | 4
+ 1000000124 | 2
+(2 rows)
+
+SELECT 'test_2colkey_def'::regclass::oid AS prev_oid \gset
+ALTER TABLE test_2colkey SPLIT PARTITION test_2colkey_def INTO
+  (PARTITION test_2colkey_2 FOR VALUES FROM (1000000101, 1) TO (1000000200, 100),
+   PARTITION test_2colkey_def DEFAULT);
+-- should be 't' (table "test_2colkey_2" after SPLIT should be the same as table
+-- "test_2colkey_def" before SPLIT):
+SELECT 'test_2colkey_2'::regclass::oid=:prev_oid;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- should be 2 rows:
+SELECT b, s FROM test_2colkey_2;
+     b      | s 
+------------+---
+ 1000000120 | 4
+ 1000000124 | 2
+(2 rows)
+
+-- should be 0 rows:
+SELECT b, s FROM test_2colkey_def;
+ b | s 
+---+---
+(0 rows)
+
+-- should be 6 rows:
+SELECT b, s FROM test_2colkey;
+     b      | s 
+------------+---
+ 1000000010 | 3
+ 1000000003 | 5
+ 1000000120 | 4
+ 1000000124 | 2
+(4 rows)
+
+--
+-- 5.1. Optimization cannot be used.
+--
+INSERT INTO test_2colkey (b, s, t) VALUES (1000000200, 1, 'value_200_1');
+SELECT 'test_2colkey_2'::regclass::oid AS prev_oid \gset
+ALTER TABLE test_2colkey SPLIT PARTITION test_2colkey_2 INTO
+  (PARTITION test_2colkey_2 FOR VALUES FROM (1000000101, 1) TO (1000000150, 100),
+   PARTITION test_2colkey_3 FOR VALUES FROM (1000000151, 1) TO (1000000200, 100));
+-- should be 'f' (optimization is not used):
+SELECT 'test_2colkey_2'::regclass::oid=:prev_oid;
+ ?column? 
+----------
+ f
+(1 row)
+
+-- should be 2 rows:
+SELECT b, s FROM test_2colkey_2;
+     b      | s 
+------------+---
+ 1000000120 | 4
+ 1000000124 | 2
+(2 rows)
+
+-- should be 1 row:
+SELECT b, s FROM test_2colkey_3;
+     b      | s 
+------------+---
+ 1000000200 | 1
+(1 row)
+
+DROP TABLE test_2colkey CASCADE;
+--
 DROP SCHEMA partition_split_schema;
diff --git a/src/test/regress/sql/partition_split.sql b/src/test/regress/sql/partition_split.sql
index e33e616230..0208c0158c 100644
--- a/src/test/regress/sql/partition_split.sql
+++ b/src/test/regress/sql/partition_split.sql
@@ -788,5 +788,157 @@ SELECT * FROM sales_others;
 
 DROP TABLE sales_range;
 
+--
+--
+-- Tests for SPLIT optimization (BY RANGE partitioning): if one of the new
+-- partitions contains all the rows of the split partition, then we can rename
+-- the split partition instead of creating a new partition and moving the rows.
+--
+-- 1. Optimization should be used.
+--
+CREATE TABLE test(name text, i int) PARTITION BY RANGE (i);
+CREATE TABLE test_1 PARTITION OF test FOR VALUES FROM (1) TO (10);
+CREATE TABLE test_def PARTITION OF test DEFAULT;
+CREATE INDEX idx_test_i ON test(i);
+INSERT INTO test(name, i) VALUES
+  ('a1', 1), ('a5', 5), ('a15', 15), ('a12', 12), ('a17', 17), ('a11', 11);
+-- should be rows 15, 12, 17, 11:
+SELECT i FROM test_def;
+SELECT 'test_def'::regclass::oid AS prev_oid \gset
+ALTER TABLE test SPLIT PARTITION test_def INTO 
+  (PARTITION test_def DEFAULT, PARTITION test_2 FOR VALUES FROM (11) TO (19));
+-- should be 't' (table "test_2" after SPLIT should be the same as table
+-- "test_def" before SPLIT):
+SELECT 'test_2'::regclass::oid=:prev_oid;
+-- should be rows 15, 12, 17, 11:
+SELECT i FROM test_2;
+-- should be 0 rows:
+SELECT i FROM test_def;
+-- should be 6 rows:
+SELECT * FROM test;
+DROP TABLE test CASCADE;
+--
+-- 2. Optimization cannot be used because not exists btree-index on the
+-- partition key (it is used to check the placement of rows in the partitions).
+--
+CREATE TABLE test(name text, i int) PARTITION BY RANGE (i);
+CREATE TABLE test_1 PARTITION OF test FOR VALUES FROM (1) TO (10);
+CREATE TABLE test_def PARTITION OF test DEFAULT;
+INSERT INTO test(name, i) VALUES
+  ('a1', 1), ('a5', 5), ('a15', 15), ('a12', 12), ('a17', 17), ('a11', 11);
+SELECT 'test_def'::regclass::oid AS prev_oid \gset
+ALTER TABLE test SPLIT PARTITION test_def INTO 
+  (PARTITION test_def DEFAULT, PARTITION test_2 FOR VALUES FROM (11) TO (19));
+-- should be 'f' (tables "test_2" and "test_def" should be different):
+SELECT 'test_2'::regclass::oid=:prev_oid;
+-- should be rows 15, 12, 17, 11:
+SELECT i FROM test_2;
+-- should be 6 rows:
+SELECT * FROM test;
+DROP TABLE test CASCADE;
+--
+-- 3. Optimization cannot be used because rows should be moved into different
+-- partitions.
+--
+CREATE TABLE test(name text, i int) PARTITION BY RANGE (i);
+CREATE TABLE test_1 PARTITION OF test FOR VALUES FROM (1) TO (10);
+CREATE TABLE test_def PARTITION OF test DEFAULT;
+CREATE INDEX idx_test_i ON test(i);
+INSERT INTO test(name, i) VALUES
+  ('a1', 1), ('a5', 5), ('a15', 15), ('a12', 12), ('a27', 27), ('a11', 11);
+SELECT 'test_def'::regclass::oid AS prev_oid \gset
+ALTER TABLE test SPLIT PARTITION test_def INTO 
+  (PARTITION test_def DEFAULT, PARTITION test_2 FOR VALUES FROM (11) TO (19));
+-- should be 'f' (tables "test_2" and "test_def" should be different):
+SELECT 'test_2'::regclass::oid=:prev_oid;
+-- should be rows 15, 12, 11:
+SELECT i FROM test_2;
+-- should be 6 rows:
+SELECT * FROM test;
+DROP TABLE test CASCADE;
+--
+-- 4. Optimization should be used, DEFAUT partition renames to DEFAULT
+-- partition.
+--
+CREATE TABLE test(name text, i int) PARTITION BY RANGE (i);
+CREATE TABLE test_1 PARTITION OF test FOR VALUES FROM (1) TO (10);
+CREATE TABLE test_def PARTITION OF test DEFAULT;
+CREATE INDEX idx_test_i ON test(i);
+INSERT INTO test(name, i) VALUES
+  ('a1', 1), ('a5', 5), ('a25', 25), ('a22', 22), ('a27', 27), ('a21', 21);
+-- should be rows 25, 22, 27, 21:
+SELECT i FROM test_def;
+SELECT 'test_def'::regclass::oid AS prev_oid \gset
+ALTER TABLE test SPLIT PARTITION test_def INTO 
+  (PARTITION test_def DEFAULT, PARTITION test_2 FOR VALUES FROM (11) TO (19));
+-- should be 't' (table "test_def" after SPLIT should be the same as table
+-- "test_def" before SPLIT):
+SELECT 'test_def'::regclass::oid=:prev_oid;
+-- should be 0 rows:
+SELECT i FROM test_2;
+-- should be rows 25, 22, 27, 21:
+SELECT i FROM test_def;
+-- should be 6 rows:
+SELECT * FROM test;
+DROP TABLE test CASCADE;
+--
+-- 5. Optimization should be used, 2-column partition key + different columns
+-- order in partitions.
+--
+CREATE TABLE test_2colkey(s smallint, b bigint, t text) PARTITION BY RANGE (b, s);
+CREATE TABLE test_2colkey_1 PARTITION OF test_2colkey FOR VALUES FROM (1000000001, 1) TO (1000000100, 100);
+CREATE TABLE test_2colkey_def(i int, b bigint, s smallint, t text);
+ALTER TABLE test_2colkey_def DROP COLUMN i;
+ALTER TABLE test_2colkey ATTACH PARTITION test_2colkey_def DEFAULT;
+CREATE INDEX idx_test_2colkey_s_b ON test_2colkey(b, s);
+
+INSERT INTO test_2colkey (b, s, t) VALUES (1000000010, 3, 'value_10_3');
+INSERT INTO test_2colkey (b, s, t) VALUES (1000000120, 4, 'value_120_4');
+INSERT INTO test_2colkey (b, s, t) VALUES (1000000003, 5, 'value_3_5');
+INSERT INTO test_2colkey (b, s, t) VALUES (1000000124, 2, 'value_124_2');
+
+-- should be 4 rows:
+SELECT b, s FROM test_2colkey;
+-- should be 2 rows:
+SELECT b, s FROM test_2colkey_def;
+
+SELECT 'test_2colkey_def'::regclass::oid AS prev_oid \gset
+
+ALTER TABLE test_2colkey SPLIT PARTITION test_2colkey_def INTO
+  (PARTITION test_2colkey_2 FOR VALUES FROM (1000000101, 1) TO (1000000200, 100),
+   PARTITION test_2colkey_def DEFAULT);
+
+-- should be 't' (table "test_2colkey_2" after SPLIT should be the same as table
+-- "test_2colkey_def" before SPLIT):
+SELECT 'test_2colkey_2'::regclass::oid=:prev_oid;
+
+-- should be 2 rows:
+SELECT b, s FROM test_2colkey_2;
+-- should be 0 rows:
+SELECT b, s FROM test_2colkey_def;
+-- should be 6 rows:
+SELECT b, s FROM test_2colkey;
+
+--
+-- 5.1. Optimization cannot be used.
+--
+INSERT INTO test_2colkey (b, s, t) VALUES (1000000200, 1, 'value_200_1');
+
+SELECT 'test_2colkey_2'::regclass::oid AS prev_oid \gset
+
+ALTER TABLE test_2colkey SPLIT PARTITION test_2colkey_2 INTO
+  (PARTITION test_2colkey_2 FOR VALUES FROM (1000000101, 1) TO (1000000150, 100),
+   PARTITION test_2colkey_3 FOR VALUES FROM (1000000151, 1) TO (1000000200, 100));
+
+-- should be 'f' (optimization is not used):
+SELECT 'test_2colkey_2'::regclass::oid=:prev_oid;
+
+-- should be 2 rows:
+SELECT b, s FROM test_2colkey_2;
+-- should be 1 row:
+SELECT b, s FROM test_2colkey_3;
+
+DROP TABLE test_2colkey CASCADE;
+
 --
 DROP SCHEMA partition_split_schema;
-- 
2.40.1.windows.1



Attachments:

  [text/plain] v23-0001-ALTER-TABLE-MERGE-PARTITIONS-command.patch (104.9K, ../[email protected]/2-v23-0001-ALTER-TABLE-MERGE-PARTITIONS-command.patch)
  download | inline diff:
From 3041dee1fa86363dbaed12cde1d38f061f5fda1b Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email protected]>
Date: Mon, 19 Sep 2022 16:06:03 +0300
Subject: [PATCH v23 1/4] ALTER TABLE MERGE PARTITIONS command

---
 src/backend/commands/tablecmds.c              | 351 ++++++++-
 src/backend/parser/gram.y                     |  22 +-
 src/backend/parser/parse_utilcmd.c            |  89 +++
 src/backend/partitioning/partbounds.c         | 207 +++++
 src/include/nodes/parsenodes.h                |  13 +
 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 | 736 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/partition_merge.sql      | 434 +++++++++++
 src/tools/pgindent/typedefs.list              |   1 +
 15 files changed, 2100 insertions(+), 19 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/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 68f658e834..a44a8156cf 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -662,6 +662,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
@@ -4733,6 +4735,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);
@@ -5158,6 +5164,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);
@@ -5548,6 +5559,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);
@@ -6536,6 +6555,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:
@@ -18812,6 +18833,37 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
 	}
 }
 
+/*
+ * attachPartitionTable: attach new partition to 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
  *
@@ -19014,23 +19066,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 partition to partitioned table. */
+	attachPartitionTable(wqueue, rel, attachrel, cmd->bound);
 
 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -20584,3 +20621,283 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+ * createPartitionTable: create table for new partition with given name
+ * (newPartName) like table (modelRelName)
+ *
+ * Emulates command: CREATE TABLE <newPartName> (LIKE <modelRelName>
+ * INCLUDING ALL EXCLUDING INDEXES)
+ */
+static void
+createPartitionTable(RangeVar *newPartName, RangeVar *modelRelName,
+					 AlterTableUtilityContext *context)
+{
+	CreateStmt *createStmt;
+	TableLikeClause *tlc;
+	PlannedStmt *wrapper;
+
+	createStmt = makeNode(CreateStmt);
+	createStmt->relation = newPartName;
+	createStmt->tableElts = NIL;
+	createStmt->inhRelations = NIL;
+	createStmt->constraints = NIL;
+	createStmt->options = NIL;
+	createStmt->oncommit = ONCOMMIT_NOOP;
+	createStmt->tablespacename = NULL;
+	createStmt->if_not_exists = false;
+
+	tlc = makeNode(TableLikeClause);
+	tlc->relation = modelRelName;
+
+	/*
+	 * Indexes will be inherited on "attach new partitions" stage, after data
+	 * moving.
+	 */
+	tlc->options = CREATE_TABLE_LIKE_ALL & ~CREATE_TABLE_LIKE_INDEXES;
+	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);
+}
+
+/*
+ * Struct with context of merged partition
+ */
+typedef struct MergedPartContext
+{
+	Relation	partRel;		/* relation for partition */
+}			MergedPartContext;
+
+/*
+ * moveMergedTablesRows: scan merged partitions (partContext) of partitioned
+ * table (rel) and move rows into new partition (newPartRel).
+ */
+static void
+moveMergedTablesRows(Relation rel, List *partContext, 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, partContext)
+	{
+		MergedPartContext *pc = (MergedPartContext *) lfirst(listptr);
+		TupleTableSlot *srcslot;
+		TupleConversionMap *tuple_map;
+		TableScanDesc scan;
+		Snapshot	snapshot;
+
+		/* Create tuple slot for new partition. */
+		srcslot = MakeSingleTupleTableSlot(RelationGetDescr(pc->partRel),
+										   table_slot_callbacks(pc->partRel));
+
+		/*
+		 * Map computing for moving attributes of merged partition to new
+		 * partition.
+		 */
+		tuple_map = convert_tuples_by_name(RelationGetDescr(pc->partRel),
+										   RelationGetDescr(newPartRel));
+
+		/* Scan through the rows. */
+		snapshot = RegisterSnapshot(GetLatestSnapshot());
+		scan = table_beginscan(pc->partRel, 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 for 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	   *partContexts = NIL;
+	Oid			defaultPartOid;
+	char		tmpRelName[NAMEDATALEN];
+	RangeVar   *mergePartName = cmd->name;
+	bool		isSameName = false;
+
+	/*
+	 * Lock all merged partitions, check them and create list with partitions
+	 * contexts.
+	 */
+	foreach(listptr, cmd->partlist)
+	{
+		RangeVar   *name = (RangeVar *) lfirst(listptr);
+		MergedPartContext *pc;
+
+		pc = (MergedPartContext *) palloc0(sizeof(MergedPartContext));
+
+		/*
+		 * We are going to detach and remove this partition: need to use
+		 * exclusive lock for prevent DML-queries to the partition.
+		 */
+		pc->partRel = table_openrv(name, AccessExclusiveLock);
+
+		if (pc->partRel->rd_rel->relkind != RELKIND_RELATION)
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("cannot merge non-table partition \"%s\"",
+							RelationGetRelationName(pc->partRel))));
+
+		/*
+		 * Checking that two partitions have the same name was before,
+		 * in function transformPartitionCmdForMerge().
+		 */
+		if (equal(name, cmd->name))
+			/* One new partition can have the same name as merged partition. */
+			isSameName = true;
+
+		/* Store partition context into partitions list. */
+		partContexts = lappend(partContexts, pc);
+	}
+
+	/* Detach all merged partitions. */
+	defaultPartOid =
+		get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true));
+	foreach(listptr, partContexts)
+	{
+		MergedPartContext *pc = (MergedPartContext *) lfirst(listptr);
+
+		RemoveInheritance(pc->partRel, rel, false);
+		/* Do the final part of detaching. */
+		DetachPartitionFinalize(rel, pc->partRel, false, defaultPartOid);
+	}
+
+	/* Create table for new partition, use partitioned table as model. */
+	if (isSameName)
+	{
+		/* Create partition table with generated temparary name. */
+		sprintf(tmpRelName, "merge-%u-%X-tmp", RelationGetRelid(rel), MyProcPid);
+		mergePartName = makeRangeVar(get_namespace_name(RelationGetNamespace(rel)),
+									 tmpRelName, -1);
+	}
+	createPartitionTable(mergePartName,
+						 makeRangeVar(get_namespace_name(RelationGetNamespace(rel)),
+									  RelationGetRelationName(rel), -1),
+						 context);
+
+	/* Open the new partition and acquire exclusive lock on it. */
+	newPartRel = table_openrv(mergePartName, AccessExclusiveLock);
+
+	/* Copy data from merged partitions to new partition. */
+	moveMergedTablesRows(rel, partContexts, newPartRel);
+
+	/*
+	 * Attach new partition to partitioned table. wqueue = NULL: verification
+	 * for each cloned constraint is not need.
+	 */
+	attachPartitionTable(NULL, rel, newPartRel, cmd->bound);
+
+	/* Unlock and drop merged partitions. */
+	foreach(listptr, partContexts)
+	{
+		ObjectAddress object;
+		MergedPartContext *pc = (MergedPartContext *) lfirst(listptr);
+
+		/* Get relation id before table_close() call. */
+		object.objectId = RelationGetRelid(pc->partRel);
+		object.classId = RelationRelationId;
+		object.objectSubId = 0;
+
+		/* Keep the lock until commit. */
+		table_close(pc->partRel, NoLock);
+
+		performDeletion(&object, DROP_RESTRICT, 0);
+
+		pfree(pc);
+	}
+
+	/* Rename new partition if it is needed. */
+	if (isSameName)
+	{
+		/*
+		 * We must bump the command counter to make the new partition tuple
+		 * visible for rename.
+		 */
+		CommandCounterIncrement();
+		/* Rename partition. */
+		RenameRelationInternal(RelationGetRelid(newPartRel),
+							   cmd->name->relname, false, false);
+	}
+	/* Keep the lock until commit. */
+	table_close(newPartRel, NoLock);
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 130f7fc7c3..4a5b13c300 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -744,7 +744,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
+	PARALLEL PARAMETER PARSER PARTIAL PARTITION PARTITIONS PASSING PASSWORD
 	PLACING PLANS POLICY
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
 	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
@@ -2299,6 +2299,7 @@ partition_cmd:
 					n->subtype = AT_AttachPartition;
 					cmd->name = $3;
 					cmd->bound = $4;
+					cmd->partlist = NULL;
 					cmd->concurrent = false;
 					n->def = (Node *) cmd;
 
@@ -2313,6 +2314,7 @@ partition_cmd:
 					n->subtype = AT_DetachPartition;
 					cmd->name = $3;
 					cmd->bound = NULL;
+					cmd->partlist = NULL;
 					cmd->concurrent = $4;
 					n->def = (Node *) cmd;
 
@@ -2326,6 +2328,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;
@@ -2342,6 +2359,7 @@ index_partition_cmd:
 					n->subtype = AT_AttachPartition;
 					cmd->name = $3;
 					cmd->bound = NULL;
+					cmd->partlist = NULL;
 					cmd->concurrent = false;
 					n->def = (Node *) cmd;
 
@@ -17324,6 +17342,7 @@ unreserved_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PLANS
@@ -17925,6 +17944,7 @@ bare_label_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PLACING
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 56ac4f516e..884a280869 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -58,6 +58,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"
@@ -3404,6 +3406,80 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
 }
 
 
+/*
+ * transformPartitionCmdForMerge
+ *		Analyze the ALTER TABLLE ... MERGE PARTITIONS command
+ *
+ * Does simple checks for merged partitions. Calculates bound of result
+ * 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\" 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;
+		partOids = lappend_oid(partOids, partOid);
+	}
+
+	/* Allocate bound of result 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
@@ -3674,6 +3750,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 contains 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 8a1a5a7255..b3b3aac02a 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -4978,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 equals except case
+ * "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 of 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 determine 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 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 a lower bound of merged
+				 * partition.
+				 */
+				spec->lowerdatums =
+					((PartitionBoundSpec *) list_nth(bounds, lower_bounds[0]->index))->lowerdatums;
+
+				/*
+				 * Upper bound of last partition is a 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/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 476d55dd24..edfe36bf42 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -924,6 +924,17 @@ typedef struct PartitionRangeDatum
 	int			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
  */
@@ -932,6 +943,7 @@ 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 SPLIT PARTITION command */
 	bool		concurrent;
 } PartitionCmd;
 
@@ -2248,6 +2260,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 2331acac09..985474df89 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -324,6 +324,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("placing", PLACING, RESERVED_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 b2be88ead1..1071256182 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -103,6 +103,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..ec48732c58
--- /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 trying to update inserted row and waits until s1 finished
+# 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 48563b2cf0..69920757b2 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..013ab65961
--- /dev/null
+++ b/src/test/regress/expected/partition_merge.out
@@ -0,0 +1,736 @@
+--
+-- PARTITIONS_MERGE
+-- Tests for "ALTER TABLE ... MERGE PARTITIONS ..." command
+--
+CREATE SCHEMA partitions_merge_schema;
+SET search_path = partitions_merge_schema, public;
+--
+-- BY RANGE partitioning
+--
+--
+-- Test for error codes
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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 (salesman_id INT, salesman_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" 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" already used
+LINE 1: ...e MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_feb2...
+                                                             ^
+-- ERROR:  cannot merge non-table partition "sales_apr2022"
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_apr2022) INTO sales_feb_mar_apr2022;
+ERROR:  cannot merge non-table partition "sales_apr2022"
+-- ERROR:  invalid partitions order, partition "sales_mar2022" can not be merged
+-- (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:  invalid partitions order, partition "sales_jan2022" can not be merged
+-- (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
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_jan2022) INTO sales_jan_feb_mar2022;
+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         | 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_jan_feb_mar2022 | r       | f                | FOR VALUES FROM ('01-01-2022') TO ('04-01-2022')
+ sales_others          | r       | f                | DEFAULT
+(4 rows)
+
+DROP TABLE sales_range;
+--
+-- Add rows into partitioned table then merge partitions
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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)
+
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_apr2022) INTO 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                    
+-----------------------+---------+------------------+--------------------------------------------------
+ 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_schema';
+       schemaname        |       tablename       |              indexname               | tablespace |                                                          indexdef                                                           
+-------------------------+-----------------------+--------------------------------------+------------+-----------------------------------------------------------------------------------------------------------------------------
+ partitions_merge_schema | sales_feb_mar_apr2022 | sales_feb_mar_apr2022_sales_date_idx |            | CREATE INDEX sales_feb_mar_apr2022_sales_date_idx ON partitions_merge_schema.sales_feb_mar_apr2022 USING btree (sales_date)
+(1 row)
+
+SELECT * FROM sales_range;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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_feb_mar_apr2022;
+ salesman_id | salesman_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;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+          14 | Smith         |          510 | 05-04-2022
+(1 row)
+
+-- Use indexscan for test indexes
+SET enable_indexscan = ON;
+SET enable_seqscan = OFF;
+SELECT * FROM sales_feb_mar_apr2022 where sales_date > '2022-01-01';
+ salesman_id | salesman_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)
+
+SET enable_indexscan = ON;
+SET enable_seqscan = ON;
+DROP TABLE sales_range;
+--
+-- Merge some partitions into DEFAULT partition
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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, sales_others) INTO sales_others;
+select * from sales_others;
+ salesman_id | salesman_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 (salesman_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(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2021, 12, 7);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2021, 12, 8);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 1, 1);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 2, 4);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 1, 2);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 2, 1);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 3, 3);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 3, 4);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 5, 1);
+SELECT * FROM sales_date;
+ salesman_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;
+ salesman_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;
+ salesman_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;
+ salesman_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;
+ salesman_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(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 1, 10);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 2, 10);
+SELECT * FROM sales_date;
+ salesman_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;
+ salesman_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;
+ salesman_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;
+ salesman_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 salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+CREATE TABLE salesmans01_10 PARTITION OF salesmans FOR VALUES FROM (1) TO (10);
+CREATE TABLE salesmans10_20 PARTITION OF salesmans FOR VALUES FROM (10) TO (20);
+CREATE TABLE salesmans20_30 PARTITION OF salesmans FOR VALUES FROM (20) TO (30);
+CREATE TABLE salesmans30_40 PARTITION OF salesmans FOR VALUES FROM (30) TO (40);
+INSERT INTO salesmans 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 salesmans_after_insert_statement_trigger
+    AFTER INSERT
+    ON salesmans
+    FOR EACH STATEMENT
+    EXECUTE PROCEDURE after_insert_row_trigger('salesmans');
+CREATE TRIGGER salesmans_after_insert_row_trigger
+    AFTER INSERT
+    ON salesmans
+    FOR EACH ROW
+    EXECUTE PROCEDURE after_insert_row_trigger('salesmans');
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salesmans VALUES (10, 'May');
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = ROW
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = STATEMENT
+-- 1 trigger should fire here (row):
+INSERT INTO salesmans10_20 VALUES (19, 'Ivanov');
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = ROW
+ALTER TABLE salesmans MERGE PARTITIONS (salesmans10_20, salesmans20_30, salesmans30_40) INTO salesmans10_40;
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salesmans VALUES (20, 'Smirnoff');
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = ROW
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = STATEMENT
+-- 1 trigger should fire here (row):
+INSERT INTO salesmans10_40 VALUES (30, 'Ford');
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = ROW
+SELECT * FROM salesmans01_10;
+ salesman_id | salesman_name 
+-------------+---------------
+           1 | Poirot
+(1 row)
+
+SELECT * FROM salesmans10_40;
+ salesman_id | salesman_name 
+-------------+---------------
+          10 | May
+          19 | Ivanov
+          20 | Smirnoff
+          30 | Ford
+(4 rows)
+
+DROP TABLE salesmans;
+DROP FUNCTION after_insert_row_trigger();
+--
+-- Test: merge partitions with deleted columns
+--
+CREATE TABLE salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+CREATE TABLE salesmans01_10 PARTITION OF salesmans FOR VALUES FROM (1) TO (10);
+-- Create partitions with some deleted columns:
+CREATE TABLE salesmans10_20(d1 VARCHAR(30), salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30));
+CREATE TABLE salesmans20_30(salesman_id INT PRIMARY KEY, d2 INT,  salesman_name VARCHAR(30));
+CREATE TABLE salesmans30_40(salesman_id INT PRIMARY KEY, d3 DATE, salesman_name VARCHAR(30));
+INSERT INTO salesmans10_20 VALUES ('dummy value 1', 19, 'Ivanov');
+INSERT INTO salesmans20_30 VALUES (20, 101, 'Smirnoff');
+INSERT INTO salesmans30_40 VALUES (31, now(), 'Popov');
+ALTER TABLE salesmans10_20 DROP COLUMN d1;
+ALTER TABLE salesmans20_30 DROP COLUMN d2;
+ALTER TABLE salesmans30_40 DROP COLUMN d3;
+ALTER TABLE salesmans ATTACH PARTITION salesmans10_20 FOR VALUES FROM (10) TO (20);
+ALTER TABLE salesmans ATTACH PARTITION salesmans20_30 FOR VALUES FROM (20) TO (30);
+ALTER TABLE salesmans ATTACH PARTITION salesmans30_40 FOR VALUES FROM (30) TO (40);
+INSERT INTO salesmans VALUES (1, 'Poirot');
+INSERT INTO salesmans VALUES (10, 'May');
+INSERT INTO salesmans VALUES (30, 'Ford');
+ALTER TABLE salesmans MERGE PARTITIONS (salesmans10_20, salesmans20_30, salesmans30_40) INTO salesmans10_40;
+select * from salesmans;
+ salesman_id | salesman_name 
+-------------+---------------
+           1 | Poirot
+          19 | Ivanov
+          10 | May
+          20 | Smirnoff
+          31 | Popov
+          30 | Ford
+(6 rows)
+
+select * from salesmans01_10;
+ salesman_id | salesman_name 
+-------------+---------------
+           1 | Poirot
+(1 row)
+
+select * from salesmans10_40;
+ salesman_id | salesman_name 
+-------------+---------------
+          19 | Ivanov
+          10 | May
+          20 | Smirnoff
+          31 | Popov
+          30 | Ford
+(5 rows)
+
+DROP TABLE salesmans;
+--
+-- Test: merge sub-partitions
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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 (salesman_id INT, salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+           4 | Ivanov        |          750 | 04-13-2022
+(1 row)
+
+SELECT * FROM sales_apr2022_20_30;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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
+(salesman_id INT GENERATED ALWAYS AS IDENTITY,
+ salesman_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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_west PARTITION OF sales_list FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk');
+CREATE TABLE sales_east PARTITION OF sales_list FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok');
+CREATE TABLE sales_central PARTITION OF sales_list FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd');
+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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_others2 PARTITION OF sales_list2 DEFAULT;
+CREATE TABLE sales_external (LIKE sales_list);
+CREATE TABLE sales_external2 (vch VARCHAR(5));
+-- ERROR:  partition bound for relation "sales_external" is null
+ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_external) INTO sales_all;
+ERROR:  partition bound for relation "sales_external" is null
+-- ERROR:  partition bound for relation "sales_external2" is null
+ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_external2) INTO sales_all;
+ERROR:  partition bound for relation "sales_external2" is null
+-- 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
+(salesman_id INT GENERATED ALWAYS AS IDENTITY,
+ salesman_name VARCHAR(30),
+ sales_state VARCHAR(20),
+ sales_amount INT,
+ sales_date DATE)
+PARTITION BY LIST (sales_state);
+CREATE INDEX sales_list_salesman_name_idx ON sales_list USING btree (salesman_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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_west PARTITION OF sales_list FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk');
+CREATE TABLE sales_east PARTITION OF sales_list FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok');
+CREATE TABLE sales_central PARTITION OF sales_list FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd');
+CREATE TABLE sales_others PARTITION OF sales_list DEFAULT;
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Trump',    'Magadan',        1000, '2022-03-01');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Smirnoff', 'Smolensk',       500,  '2022-03-03');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Ford',     'St. Petersburg', 2000, '2022-03-05');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Ivanov',   'Moscow',         750,  '2022-03-04');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Deev',     'Voronezh',       250,  '2022-03-07');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Poirot',   'Kazan',          1000, '2022-03-01');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Ukhta',          1200, '2022-03-06');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Li',       'Vladivostok',    1150, '2022-03-09');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Ukhta',          1200, '2022-03-11');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Halder',   'Murmansk',       800,  '2022-03-02');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Muller',   'Bryansk',        650,  '2022-03-05');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Smith',    'Volgograd',      350,  '2022-03-10');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Gandi',    'Moscow',         150,  '2022-03-08');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Plato',    'Voronezh',       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 ('Moscow', 'Kazan', 'Volgograd')
+ sales_east    | r       | f                | FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok')
+ sales_nord    | r       | f                | FOR VALUES IN ('Murmansk', 'St. Petersburg', 'Ukhta')
+ sales_west    | r       | f                | FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk')
+ 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 ('Voronezh', 'Smolensk', 'Bryansk', 'Magadan', 'Khabarovsk', 'Vladivostok', 'Moscow', 'Kazan', 'Volgograd')
+ sales_nord   | r       | f                | FOR VALUES IN ('Murmansk', 'St. Petersburg', 'Ukhta')
+ sales_others | r       | f                | DEFAULT
+(3 rows)
+
+SELECT * FROM sales_list;
+ salesman_id | salesman_name |  sales_state   | sales_amount | sales_date 
+-------------+---------------+----------------+--------------+------------
+           2 | Smirnoff      | Smolensk       |          500 | 03-03-2022
+           5 | Deev          | Voronezh       |          250 | 03-07-2022
+          11 | Muller        | Bryansk        |          650 | 03-05-2022
+          14 | Plato         | Voronezh       |          950 | 03-05-2022
+           1 | Trump         | Magadan        |         1000 | 03-01-2022
+           8 | Li            | Vladivostok    |         1150 | 03-09-2022
+           4 | Ivanov        | Moscow         |          750 | 03-04-2022
+           6 | Poirot        | Kazan          |         1000 | 03-01-2022
+          12 | Smith         | Volgograd      |          350 | 03-10-2022
+          13 | Gandi         | Moscow         |          150 | 03-08-2022
+           3 | Ford          | St. Petersburg |         2000 | 03-05-2022
+           7 | May           | Ukhta          |         1200 | 03-06-2022
+           9 | May           | Ukhta          |         1200 | 03-11-2022
+          10 | Halder        | Murmansk       |          800 | 03-02-2022
+(14 rows)
+
+SELECT * FROM sales_nord;
+ salesman_id | salesman_name |  sales_state   | sales_amount | sales_date 
+-------------+---------------+----------------+--------------+------------
+           3 | Ford          | St. Petersburg |         2000 | 03-05-2022
+           7 | May           | Ukhta          |         1200 | 03-06-2022
+           9 | May           | Ukhta          |         1200 | 03-11-2022
+          10 | Halder        | Murmansk       |          800 | 03-02-2022
+(4 rows)
+
+SELECT * FROM sales_all;
+ salesman_id | salesman_name | sales_state | sales_amount | sales_date 
+-------------+---------------+-------------+--------------+------------
+           2 | Smirnoff      | Smolensk    |          500 | 03-03-2022
+           5 | Deev          | Voronezh    |          250 | 03-07-2022
+          11 | Muller        | Bryansk     |          650 | 03-05-2022
+          14 | Plato         | Voronezh    |          950 | 03-05-2022
+           1 | Trump         | Magadan     |         1000 | 03-01-2022
+           8 | Li            | Vladivostok |         1150 | 03-09-2022
+           4 | Ivanov        | Moscow      |          750 | 03-04-2022
+           6 | Poirot        | Kazan       |         1000 | 03-01-2022
+          12 | Smith         | Volgograd   |          350 | 03-10-2022
+          13 | Gandi         | Moscow      |          150 | 03-08-2022
+(10 rows)
+
+-- Use indexscan for test indexes after merge partitions
+SET enable_indexscan = ON;
+SET enable_seqscan = OFF;
+SELECT * FROM sales_all WHERE sales_state = 'Moscow';
+ salesman_id | salesman_name | sales_state | sales_amount | sales_date 
+-------------+---------------+-------------+--------------+------------
+           4 | Ivanov        | Moscow      |          750 | 03-04-2022
+          13 | Gandi         | Moscow      |          150 | 03-08-2022
+(2 rows)
+
+SELECT * FROM sales_list WHERE sales_state = 'Moscow';
+ salesman_id | salesman_name | sales_state | sales_amount | sales_date 
+-------------+---------------+-------------+--------------+------------
+           4 | Ivanov        | Moscow      |          750 | 03-04-2022
+          13 | Gandi         | Moscow      |          150 | 03-08-2022
+(2 rows)
+
+SELECT * FROM sales_list WHERE salesman_name = 'Ivanov';
+ salesman_id | salesman_name | sales_state | sales_amount | sales_date 
+-------------+---------------+-------------+--------------+------------
+           4 | Ivanov        | Moscow      |          750 | 03-04-2022
+(1 row)
+
+SET enable_indexscan = ON;
+SET enable_seqscan = ON;
+DROP TABLE sales_list;
+--
+DROP SCHEMA partitions_merge_schema;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 1d8a414eea..6f0236d207 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..8bf90fd621
--- /dev/null
+++ b/src/test/regress/sql/partition_merge.sql
@@ -0,0 +1,434 @@
+--
+-- PARTITIONS_MERGE
+-- Tests for "ALTER TABLE ... MERGE PARTITIONS ..." command
+--
+
+CREATE SCHEMA partitions_merge_schema;
+SET search_path = partitions_merge_schema, public;
+
+--
+-- BY RANGE partitioning
+--
+
+--
+-- Test for error codes
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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 (salesman_id INT, salesman_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" already used
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_feb2022) INTO sales_feb_mar_apr2022;
+-- ERROR:  cannot merge non-table partition "sales_apr2022"
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_apr2022) INTO sales_feb_mar_apr2022;
+-- ERROR:  invalid partitions order, partition "sales_mar2022" can not be merged
+-- (space between sections sales_jan2022 and sales_mar2022)
+ALTER TABLE sales_range MERGE PARTITIONS (sales_jan2022, sales_mar2022) INTO sales_jan_mar2022;
+-- ERROR:  invalid partitions order, partition "sales_jan2022" can not be merged
+-- (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
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_jan2022) INTO sales_jan_feb_mar2022;
+
+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 (salesman_id INT, salesman_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;
+
+ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_apr2022) INTO 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_schema';
+
+SELECT * FROM sales_range;
+SELECT * FROM sales_jan2022;
+SELECT * FROM sales_feb_mar_apr2022;
+SELECT * FROM sales_others;
+
+-- Use indexscan for test indexes
+SET enable_indexscan = ON;
+SET enable_seqscan = OFF;
+
+SELECT * FROM sales_feb_mar_apr2022 where sales_date > '2022-01-01';
+
+SET enable_indexscan = ON;
+SET enable_seqscan = ON;
+
+DROP TABLE sales_range;
+
+--
+-- Merge some partitions into DEFAULT partition
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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, 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 (salesman_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(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2021, 12, 7);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2021, 12, 8);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 1, 1);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 2, 4);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 1, 2);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 2, 1);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 3, 3);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 3, 4);
+INSERT INTO sales_date(salesman_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(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 1, 10);
+INSERT INTO sales_date(salesman_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 salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+
+CREATE TABLE salesmans01_10 PARTITION OF salesmans FOR VALUES FROM (1) TO (10);
+CREATE TABLE salesmans10_20 PARTITION OF salesmans FOR VALUES FROM (10) TO (20);
+CREATE TABLE salesmans20_30 PARTITION OF salesmans FOR VALUES FROM (20) TO (30);
+CREATE TABLE salesmans30_40 PARTITION OF salesmans FOR VALUES FROM (30) TO (40);
+
+INSERT INTO salesmans 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 salesmans_after_insert_statement_trigger
+    AFTER INSERT
+    ON salesmans
+    FOR EACH STATEMENT
+    EXECUTE PROCEDURE after_insert_row_trigger('salesmans');
+
+CREATE TRIGGER salesmans_after_insert_row_trigger
+    AFTER INSERT
+    ON salesmans
+    FOR EACH ROW
+    EXECUTE PROCEDURE after_insert_row_trigger('salesmans');
+
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salesmans VALUES (10, 'May');
+-- 1 trigger should fire here (row):
+INSERT INTO salesmans10_20 VALUES (19, 'Ivanov');
+
+ALTER TABLE salesmans MERGE PARTITIONS (salesmans10_20, salesmans20_30, salesmans30_40) INTO salesmans10_40;
+
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salesmans VALUES (20, 'Smirnoff');
+-- 1 trigger should fire here (row):
+INSERT INTO salesmans10_40 VALUES (30, 'Ford');
+
+SELECT * FROM salesmans01_10;
+SELECT * FROM salesmans10_40;
+
+DROP TABLE salesmans;
+DROP FUNCTION after_insert_row_trigger();
+
+--
+-- Test: merge partitions with deleted columns
+--
+CREATE TABLE salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+
+CREATE TABLE salesmans01_10 PARTITION OF salesmans FOR VALUES FROM (1) TO (10);
+-- Create partitions with some deleted columns:
+CREATE TABLE salesmans10_20(d1 VARCHAR(30), salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30));
+CREATE TABLE salesmans20_30(salesman_id INT PRIMARY KEY, d2 INT,  salesman_name VARCHAR(30));
+CREATE TABLE salesmans30_40(salesman_id INT PRIMARY KEY, d3 DATE, salesman_name VARCHAR(30));
+
+INSERT INTO salesmans10_20 VALUES ('dummy value 1', 19, 'Ivanov');
+INSERT INTO salesmans20_30 VALUES (20, 101, 'Smirnoff');
+INSERT INTO salesmans30_40 VALUES (31, now(), 'Popov');
+
+ALTER TABLE salesmans10_20 DROP COLUMN d1;
+ALTER TABLE salesmans20_30 DROP COLUMN d2;
+ALTER TABLE salesmans30_40 DROP COLUMN d3;
+
+ALTER TABLE salesmans ATTACH PARTITION salesmans10_20 FOR VALUES FROM (10) TO (20);
+ALTER TABLE salesmans ATTACH PARTITION salesmans20_30 FOR VALUES FROM (20) TO (30);
+ALTER TABLE salesmans ATTACH PARTITION salesmans30_40 FOR VALUES FROM (30) TO (40);
+
+INSERT INTO salesmans VALUES (1, 'Poirot');
+INSERT INTO salesmans VALUES (10, 'May');
+INSERT INTO salesmans VALUES (30, 'Ford');
+
+ALTER TABLE salesmans MERGE PARTITIONS (salesmans10_20, salesmans20_30, salesmans30_40) INTO salesmans10_40;
+
+select * from salesmans;
+select * from salesmans01_10;
+select * from salesmans10_40;
+
+DROP TABLE salesmans;
+
+--
+-- Test: merge sub-partitions
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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 (salesman_id INT, salesman_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
+(salesman_id INT GENERATED ALWAYS AS IDENTITY,
+ salesman_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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_west PARTITION OF sales_list FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk');
+CREATE TABLE sales_east PARTITION OF sales_list FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok');
+CREATE TABLE sales_central PARTITION OF sales_list FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd');
+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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_others2 PARTITION OF sales_list2 DEFAULT;
+
+
+CREATE TABLE sales_external (LIKE sales_list);
+CREATE TABLE sales_external2 (vch VARCHAR(5));
+
+-- ERROR:  partition bound for relation "sales_external" is null
+ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_external) INTO sales_all;
+-- ERROR:  partition bound for relation "sales_external2" is null
+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
+(salesman_id INT GENERATED ALWAYS AS IDENTITY,
+ salesman_name VARCHAR(30),
+ sales_state VARCHAR(20),
+ sales_amount INT,
+ sales_date DATE)
+PARTITION BY LIST (sales_state);
+
+CREATE INDEX sales_list_salesman_name_idx ON sales_list USING btree (salesman_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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_west PARTITION OF sales_list FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk');
+CREATE TABLE sales_east PARTITION OF sales_list FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok');
+CREATE TABLE sales_central PARTITION OF sales_list FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd');
+CREATE TABLE sales_others PARTITION OF sales_list DEFAULT;
+
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Trump',    'Magadan',        1000, '2022-03-01');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Smirnoff', 'Smolensk',       500,  '2022-03-03');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Ford',     'St. Petersburg', 2000, '2022-03-05');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Ivanov',   'Moscow',         750,  '2022-03-04');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Deev',     'Voronezh',       250,  '2022-03-07');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Poirot',   'Kazan',          1000, '2022-03-01');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Ukhta',          1200, '2022-03-06');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Li',       'Vladivostok',    1150, '2022-03-09');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Ukhta',          1200, '2022-03-11');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Halder',   'Murmansk',       800,  '2022-03-02');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Muller',   'Bryansk',        650,  '2022-03-05');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Smith',    'Volgograd',      350,  '2022-03-10');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Gandi',    'Moscow',         150,  '2022-03-08');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Plato',    'Voronezh',       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 test indexes after merge partitions
+SET enable_indexscan = ON;
+SET enable_seqscan = OFF;
+
+SELECT * FROM sales_all WHERE sales_state = 'Moscow';
+SELECT * FROM sales_list WHERE sales_state = 'Moscow';
+SELECT * FROM sales_list WHERE salesman_name = 'Ivanov';
+
+SET enable_indexscan = ON;
+SET enable_seqscan = ON;
+
+DROP TABLE sales_list;
+
+--
+DROP SCHEMA partitions_merge_schema;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 7e866e3c3d..e827ed0214 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2578,6 +2578,7 @@ SimpleStats
 SimpleStringList
 SimpleStringListCell
 SingleBoundSortItem
+SinglePartitionSpec
 Size
 SkipPages
 SlabBlock
-- 
2.40.1.windows.1



  [text/plain] v23-0002-ALTER-TABLE-SPLIT-PARTITION-command.patch (169.5K, ../[email protected]/3-v23-0002-ALTER-TABLE-SPLIT-PARTITION-command.patch)
  download | inline diff:
From 2439724753c8ac6201685a3d34964f9af256aabf Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email protected]>
Date: Mon, 19 Sep 2022 17:58:52 +0300
Subject: [PATCH v23 2/4] ALTER TABLE SPLIT PARTITION command

---
 src/backend/commands/tablecmds.c              |  407 +++++
 src/backend/parser/gram.y                     |   38 +-
 src/backend/parser/parse_utilcmd.c            |   62 +-
 src/backend/partitioning/partbounds.c         |  655 ++++++++
 src/backend/utils/adt/ruleutils.c             |   18 +
 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 | 1317 +++++++++++++++++
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/partition_split.sql      |  792 ++++++++++
 16 files changed, 3536 insertions(+), 12 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/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index a44a8156cf..b0d2e4af91 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -662,6 +662,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);
 
@@ -4735,6 +4738,10 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = ShareUpdateExclusiveLock;
 				break;
 
+			case AT_SplitPartition:
+				cmd_lockmode = AccessExclusiveLock;
+				break;
+
 			case AT_MergePartitions:
 				cmd_lockmode = AccessExclusiveLock;
 				break;
@@ -5164,6 +5171,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 */
@@ -5559,6 +5571,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);
@@ -6555,6 +6575,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:
@@ -20622,6 +20644,255 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 	return cstorage;
 }
 
+/*
+ * Struct with context of new partition for insert rows from splited partition
+ */
+typedef struct SplitPartitionContext
+{
+	ExprState  *partqualstate;	/* expression for check slot for partition
+								 * (NULL for DEFAULT partition) */
+	BulkInsertState bistate;	/* state of bulk inserts for partition */
+	TupleTableSlot *dstslot;	/* slot for insert 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 Relation's.
+ * 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 for 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 new partition with given name
  * (newPartName) like table (modelRelName)
@@ -20676,6 +20947,142 @@ createPartitionTable(RangeVar *newPartName, RangeVar *modelRelName,
 				   NULL);
 }
 
+/*
+ * 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;
+	RangeVar   *splitPartName = cmd->name;
+	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 prevent DML-queries to the partition.
+	 */
+	splitRel = table_openrv(splitPartName, AccessExclusiveLock);
+
+	if (splitRel->rd_rel->relkind != RELKIND_RELATION)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("cannot split non-table partition \"%s\"",
+						RelationGetRelationName(splitRel))));
+
+	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.
+		 */
+		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 reuse name.
+	 */
+	if (isSameName)
+	{
+		/*
+		 * We must bump the command counter to make the split partition tuple
+		 * visible for rename.
+		 */
+		CommandCounterIncrement();
+		/* Rename partition. */
+		sprintf(tmpRelName, "split-%u-%X-tmp", RelationGetRelid(rel), MyProcPid);
+		RenameRelationInternal(splitRelOid, tmpRelName, false, false);
+		splitPartName = makeRangeVar(
+									 get_namespace_name(RelationGetNamespace(splitRel)),
+									 tmpRelName, -1);
+
+		/*
+		 * We must bump the command counter to make the split partition tuple
+		 * visible after rename.
+		 */
+		CommandCounterIncrement();
+	}
+
+	/* Create new partitions (like split partition), without indexes. */
+	foreach(listptr, cmd->partlist)
+	{
+		SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
+		Relation	newPartRel;
+
+		createPartitionTable(sps->name, splitPartName, context);
+
+		/* Open the new partition and acquire exclusive lock on it. */
+		newPartRel = table_openrv(sps->name, AccessExclusiveLock);
+
+		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 need. */
+		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);
+}
+
 /*
  * Struct with context of merged partition
  */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4a5b13c300..6da1f9a076 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -270,6 +270,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;
@@ -643,6 +644,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
 
@@ -759,7 +762,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 SQL_P STABLE STANDALONE_P
+	SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SPLIT SQL_P STABLE STANDALONE_P
 	START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRIP_P
 	SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER
 
@@ -2289,6 +2292,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
@@ -2333,6 +2353,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
 				{
@@ -17407,6 +17441,7 @@ unreserved_keyword:
 			| SIMPLE
 			| SKIP
 			| SNAPSHOT
+			| SPLIT
 			| SQL_P
 			| STABLE
 			| STANDALONE_P
@@ -18021,6 +18056,7 @@ bare_label_keyword:
 			| SMALLINT
 			| SNAPSHOT
 			| SOME
+			| SPLIT
 			| SQL_P
 			| STABLE
 			| STANDALONE_P
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 884a280869..c5a794e237 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);
@@ -3406,6 +3406,43 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
 }
 
 
+/*
+ * transformPartitionCmdForSplit
+ *		Analyze the ALTER TABLLE ... 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);
+
+	/* Then we should check partitions with transformed bounds. */
+	check_partitions_for_split(parent, splitPartOid, partcmd->name, partcmd->partlist, cxt->pstate);
+}
+
+
 /*
  * transformPartitionCmdForMerge
  *		Analyze the ALTER TABLLE ... MERGE PARTITIONS command
@@ -3742,7 +3779,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;
 				}
@@ -3750,6 +3787,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 				newcmds = lappend(newcmds, cmd);
 				break;
 
+			case AT_SplitPartition:
 			case AT_MergePartitions:
 				{
 					PartitionCmd *partcmd = (PartitionCmd *) cmd->def;
@@ -3758,7 +3796,11 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 						ereport(ERROR,
 								(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 								 errmsg("list of new partitions should contains at least two items")));
-					transformPartitionCmdForMerge(&cxt, partcmd);
+
+					if (cmd->subtype == AT_SplitPartition)
+						transformPartitionCmdForSplit(&cxt, partcmd);
+					else
+						transformPartitionCmdForMerge(&cxt, partcmd);
 					newcmds = lappend(newcmds, cmd);
 					break;
 				}
@@ -4163,13 +4205,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;
 
@@ -4178,9 +4220,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:
 
@@ -4188,7 +4230,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 b3b3aac02a..e3504bfe26 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,602 @@ 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" is 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 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);
+		bool		overlap = false;
+
+		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 && cmpval) || (defaultPart && cmpval < 0))
+				overlap = true;
+		}
+		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 of "spec" should be equal (or less than or equal
+			 * in case defaultPart=true) to upper bound of split partition.
+			 */
+			if ((!defaultPart && cmpval) || (defaultPart && cmpval > 0))
+				overlap = true;
+		}
+
+		if (overlap)
+		{
+			PartitionRangeDatum *datum;
+
+			datum = list_nth(first ? spec->lowerdatums : spec->upperdatums, abs(cmpval) - 1);
+
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("%s bound of partition \"%s\" is %s %s bound of split partition",
+							first ? "lower" : "upper",
+							relname,
+							defaultPart ? (first ? "less than" : "greater than") : "not equals to",
+							first ? "lower" : "upper"),
+					 parser_errposition(pstate, datum->location)));
+		}
+	}
+}
+
+/*
+ * check_partition_bounds_for_split_list
+ *
+ * (function for BY LIST partitioning)
+ *
+ * Checks that bounds of new partition is 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
+ *
+ * (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 it contains
+	 * split partition (partOid).
+	 */
+	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
+	 * partitionde 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 get 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 not have value %s but split partition has",
+						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 = -1;
+				foreach(listptr, partlist)
+				{
+					SinglePartitionSpec *sps =
+					(SinglePartitionSpec *) lfirst(listptr);
+
+					i++;
+					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;
+						continue;
+					}
+					new_parts[nparts++] = sps;
+				}
+			}
+			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("any 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\" 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 b625f471a8..110c267823 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -12653,3 +12653,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/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index edfe36bf42..d0ffa3e9de 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2260,6 +2260,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 985474df89..89e0a5c504 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -404,6 +404,7 @@ PG_KEYWORD("skip", SKIP, UNRESERVED_KEYWORD, BARE_LABEL)
 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("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 1071256182..79b730fb86 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -104,6 +104,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 69920757b2..265ef2a547 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..38736d3e67
--- /dev/null
+++ b/src/test/regress/expected/partition_split.out
@@ -0,0 +1,1317 @@
+--
+-- PARTITION_SPLIT
+-- Tests for "ALTER TABLE ... SPLIT PARTITION ..." command
+--
+CREATE SCHEMA partition_split_schema;
+SET search_path = partition_split_schema, public;
+--
+-- BY RANGE partitioning
+--
+--
+-- Test for error codes
+--
+CREATE TABLE sales_range (salesman_id int, salesman_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 contains 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 contains 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" 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" already used
+LINE 3:    PARTITION sales_feb_mar_apr2022 FOR VALUES FROM ('2022-03...
+                     ^
+-- ERROR:  name "sales_feb2022" 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" 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" is not equals to upper bound of previous 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-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 equals 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 equals to lower bound of split partition
+LINE 2:   (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-02') TO...
+                                                    ^
+DROP TABLE sales_range;
+DROP TABLE sales_others;
+--
+-- Add rows into partitioned table then split partition
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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 (salesman_id INT, salesman_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;
+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'));
+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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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 (salesman_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(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2021, 12, 7);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2021, 12, 8);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 1, 1);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 2, 4);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 1, 2);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 2, 1);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 3, 3);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 3, 4);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 5, 1);
+SELECT * FROM sales_date;
+ salesman_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;
+ salesman_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;
+ salesman_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;
+ salesman_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(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 1, 10);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 2, 10);
+SELECT * FROM sales_date;
+ salesman_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;
+ salesman_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;
+ salesman_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;
+ salesman_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;
+ salesman_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; using a index on partition key; check index after split
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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;
+ salesman_id | salesman_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';
+       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 test indexes
+SET enable_indexscan = ON;
+SET enable_seqscan = OFF;
+SELECT * FROM sales_feb2022 where sales_date > '2022-01-01';
+ salesman_id | salesman_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';
+ salesman_id | salesman_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';
+ salesman_id | salesman_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';
+ salesman_id | salesman_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';
+       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';
+       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';
+       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';
+       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 split DEFAULT partition (different bounds)
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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 (exact 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:  any partition 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:  any 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 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 (salesman_id INT, salesman_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 equals 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 salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30));
+INSERT INTO salesmans VALUES (1,  'Poirot');
+CREATE TABLE sales_range (
+salesman_id INT REFERENCES salesmans(salesman_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;
+                    pg_get_constraintdef                     |            conname             | conkey 
+-------------------------------------------------------------+--------------------------------+--------
+ CHECK ((sales_amount > 1))                                  | sales_range_sales_amount_check | {2}
+ FOREIGN KEY (salesman_id) REFERENCES salesmans(salesman_id) | sales_range_salesman_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;
+                    pg_get_constraintdef                     |            conname             | conkey 
+-------------------------------------------------------------+--------------------------------+--------
+ CHECK ((sales_amount > 1))                                  | sales_range_sales_amount_check | {2}
+ FOREIGN KEY (salesman_id) REFERENCES salesmans(salesman_id) | sales_range_salesman_id_fkey   | {1}
+(2 rows)
+
+SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_mar2022'::regclass::oid;
+                    pg_get_constraintdef                     |            conname             | conkey 
+-------------------------------------------------------------+--------------------------------+--------
+ CHECK ((sales_amount > 1))                                  | sales_range_sales_amount_check | {2}
+ FOREIGN KEY (salesman_id) REFERENCES salesmans(salesman_id) | sales_range_salesman_id_fkey   | {1}
+(2 rows)
+
+SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_apr2022'::regclass::oid;
+                    pg_get_constraintdef                     |            conname             | conkey 
+-------------------------------------------------------------+--------------------------------+--------
+ CHECK ((sales_amount > 1))                                  | sales_range_sales_amount_check | {2}
+ FOREIGN KEY (salesman_id) REFERENCES salesmans(salesman_id) | sales_range_salesman_id_fkey   | {1}
+(2 rows)
+
+-- ERROR:  new row for relation "sales_mar2022" violates check constraint "sales_range_salesman_id_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_salesman_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_salesman_id_fkey"
+DETAIL:  Key (salesman_id)=(-1) is not present in table "salesmans".
+-- ok
+INSERT INTO sales_range VALUES (1, 10, '2022-03-11');
+DROP TABLE sales_range CASCADE;
+DROP TABLE salesmans CASCADE;
+--
+-- Test: split partition on partitioned table in case exists FOREIGN KEY reference from another table
+--
+CREATE TABLE salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+CREATE TABLE sales (salesman_id INT REFERENCES salesmans(salesman_id), sales_amount INT, sales_date DATE);
+CREATE TABLE salesmans01_10 PARTITION OF salesmans FOR VALUES FROM (1) TO (10);
+CREATE TABLE salesmans10_40 PARTITION OF salesmans FOR VALUES FROM (10) TO (40);
+INSERT INTO salesmans VALUES (1,  'Poirot');
+INSERT INTO salesmans VALUES (10, 'May');
+INSERT INTO salesmans VALUES (19, 'Ivanov');
+INSERT INTO salesmans VALUES (20, 'Smirnoff');
+INSERT INTO salesmans 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 salesmans01_10;
+ salesman_id | salesman_name 
+-------------+---------------
+           1 | Poirot
+(1 row)
+
+SELECT * FROM salesmans10_40;
+ salesman_id | salesman_name 
+-------------+---------------
+          10 | May
+          19 | Ivanov
+          20 | Smirnoff
+          30 | Ford
+(4 rows)
+
+ALTER TABLE salesmans SPLIT PARTITION salesmans10_40 INTO
+  (PARTITION salesmans10_20 FOR VALUES FROM (10) TO (20),
+   PARTITION salesmans20_30 FOR VALUES FROM (20) TO (30),
+   PARTITION salesmans30_40 FOR VALUES FROM (30) TO (40));
+SELECT * FROM salesmans01_10;
+ salesman_id | salesman_name 
+-------------+---------------
+           1 | Poirot
+(1 row)
+
+SELECT * FROM salesmans10_20;
+ salesman_id | salesman_name 
+-------------+---------------
+          10 | May
+          19 | Ivanov
+(2 rows)
+
+SELECT * FROM salesmans20_30;
+ salesman_id | salesman_name 
+-------------+---------------
+          20 | Smirnoff
+(1 row)
+
+SELECT * FROM salesmans30_40;
+ salesman_id | salesman_name 
+-------------+---------------
+          30 | Ford
+(1 row)
+
+-- ERROR:  insert or update on table "sales" violates foreign key constraint "sales_salesman_id_fkey"
+INSERT INTO sales VALUES (40, 50,  '2022-03-04');
+ERROR:  insert or update on table "sales" violates foreign key constraint "sales_salesman_id_fkey"
+DETAIL:  Key (salesman_id)=(40) is not present in table "salesmans".
+-- ok
+INSERT INTO sales VALUES (30, 50,  '2022-03-04');
+DROP TABLE sales CASCADE;
+DROP TABLE salesmans CASCADE;
+--
+-- Test: split partition of partitioned table with triggers
+--
+CREATE TABLE salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+CREATE TABLE salesmans01_10 PARTITION OF salesmans FOR VALUES FROM (1) TO (10);
+CREATE TABLE salesmans10_40 PARTITION OF salesmans FOR VALUES FROM (10) TO (40);
+INSERT INTO salesmans 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 salesmans_after_insert_statement_trigger
+    AFTER INSERT
+    ON salesmans
+    FOR EACH STATEMENT
+    EXECUTE PROCEDURE after_insert_row_trigger('salesmans');
+CREATE TRIGGER salesmans_after_insert_row_trigger
+    AFTER INSERT
+    ON salesmans
+    FOR EACH ROW
+    EXECUTE PROCEDURE after_insert_row_trigger('salesmans');
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salesmans VALUES (10, 'May');
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = ROW
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = STATEMENT
+-- 1 trigger should fire here (row):
+INSERT INTO salesmans10_40 VALUES (19, 'Ivanov');
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = ROW
+ALTER TABLE salesmans SPLIT PARTITION salesmans10_40 INTO
+  (PARTITION salesmans10_20 FOR VALUES FROM (10) TO (20),
+   PARTITION salesmans20_30 FOR VALUES FROM (20) TO (30),
+   PARTITION salesmans30_40 FOR VALUES FROM (30) TO (40));
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salesmans VALUES (20, 'Smirnoff');
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = ROW
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = STATEMENT
+-- 1 trigger should fire here (row):
+INSERT INTO salesmans30_40 VALUES (30, 'Ford');
+NOTICE:  trigger(salesmans) called: action = INSERT, when = AFTER, level = ROW
+SELECT * FROM salesmans01_10;
+ salesman_id | salesman_name 
+-------------+---------------
+           1 | Poirot
+(1 row)
+
+SELECT * FROM salesmans10_20;
+ salesman_id | salesman_name 
+-------------+---------------
+          10 | May
+          19 | Ivanov
+(2 rows)
+
+SELECT * FROM salesmans20_30;
+ salesman_id | salesman_name 
+-------------+---------------
+          20 | Smirnoff
+(1 row)
+
+SELECT * FROM salesmans30_40;
+ salesman_id | salesman_name 
+-------------+---------------
+          30 | Ford
+(1 row)
+
+DROP TABLE salesmans CASCADE;
+DROP FUNCTION after_insert_row_trigger();
+--
+-- Test: split partition with deleted columns
+--
+CREATE TABLE salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+CREATE TABLE salesmans01_10 PARTITION OF salesmans FOR VALUES FROM (1) TO (10);
+-- Create new partition with some deleted columns:
+CREATE TABLE salesmans10_40(d1 VARCHAR(30), salesman_id INT PRIMARY KEY, d2 INT, d3 DATE, salesman_name VARCHAR(30));
+INSERT INTO salesmans10_40 VALUES ('dummy value 1', 19, 100, now(), 'Ivanov');
+INSERT INTO salesmans10_40 VALUES ('dummy value 2', 20, 101, now(), 'Smirnoff');
+ALTER TABLE salesmans10_40 DROP COLUMN d1;
+ALTER TABLE salesmans10_40 DROP COLUMN d2;
+ALTER TABLE salesmans10_40 DROP COLUMN d3;
+ALTER TABLE salesmans ATTACH PARTITION salesmans10_40 FOR VALUES FROM (10) TO (40);
+INSERT INTO salesmans VALUES (1, 'Poirot');
+INSERT INTO salesmans VALUES (10, 'May');
+INSERT INTO salesmans VALUES (30, 'Ford');
+ALTER TABLE salesmans SPLIT PARTITION salesmans10_40 INTO
+  (PARTITION salesmans10_20 FOR VALUES FROM (10) TO (20),
+   PARTITION salesmans20_30 FOR VALUES FROM (20) TO (30),
+   PARTITION salesmans30_40 FOR VALUES FROM (30) TO (40));
+select * from salesmans01_10;
+ salesman_id | salesman_name 
+-------------+---------------
+           1 | Poirot
+(1 row)
+
+select * from salesmans10_20;
+ salesman_id | salesman_name 
+-------------+---------------
+          19 | Ivanov
+          10 | May
+(2 rows)
+
+select * from salesmans20_30;
+ salesman_id | salesman_name 
+-------------+---------------
+          20 | Smirnoff
+(1 row)
+
+select * from salesmans30_40;
+ salesman_id | salesman_name 
+-------------+---------------
+          30 | Ford
+(1 row)
+
+DROP TABLE salesmans CASCADE;
+--
+-- Test: split sub-partition
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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 (salesman_id INT, salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+           4 | Ivanov        |          750 | 04-13-2022
+(1 row)
+
+SELECT * FROM sales_apr2022_20_30;
+ salesman_id | salesman_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
+(salesman_id INT,
+ salesman_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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Moscow', 'Voronezh', 'Smolensk', 'Bryansk', 'Magadan', 'Kazan', 'Khabarovsk', 'Volgograd', 'Vladivostok');
+CREATE TABLE sales_others PARTITION OF sales_list DEFAULT;
+-- ERROR:  partition "sales_east" would overlap partition "sales_nord"
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk'),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok', 'Murmansk'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd'));
+ERROR:  new partition "sales_east" would overlap with another (not split) partition "sales_nord"
+LINE 3: ...ALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok', 'Murmansk'...
+                                                             ^
+-- 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 ('Voronezh', 'Smolensk', 'Bryansk'),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Voronezh', 'Volgograd'));
+ERROR:  new partition "sales_west" would overlap with another new partition "sales_central"
+LINE 2:   (PARTITION sales_west FOR VALUES IN ('Voronezh', 'Smolensk...
+                                               ^
+-- 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 ('Voronezh', 'Smolensk', 'Bryansk', NULL),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd'));
+ERROR:  new partition "sales_west" cannot have NULL value because split partition does not have
+LINE 2: ...est FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk', NULL),
+                                                                 ^
+DROP TABLE sales_list;
+--
+-- Test: two specific errors for BY LIST partitioning:
+--   * new partitions not has NULL value that split partition has.
+--   * new partitions not has a value that split partition has.
+--
+CREATE TABLE sales_list
+(salesman_id INT,
+ salesman_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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Moscow', 'Voronezh', 'Smolensk', 'Bryansk', 'Magadan', 'Kazan', 'Khabarovsk', 'Volgograd', 'Vladivostok', NULL);
+-- ERROR:  new partitions not have value NULL but split partition has
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk'),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd'));
+ERROR:  new partitions not have value NULL but split partition has
+-- ERROR:  new partitions not have value 'Volgograd' but split partition has
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk'),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', NULL));
+ERROR:  new partitions not have value 'Volgograd' but split partition has
+DROP TABLE sales_list;
+--
+-- Test: BY LIST partitioning, SPLIT PARTITION with data
+--
+CREATE TABLE sales_list
+(salesman_id SERIAL,
+ salesman_name VARCHAR(30),
+ sales_state VARCHAR(20),
+ sales_amount INT,
+ sales_date DATE)
+PARTITION BY LIST (sales_state);
+CREATE INDEX sales_list_salesman_name_idx ON sales_list USING btree (salesman_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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Moscow', 'Voronezh', 'Smolensk', 'Bryansk', 'Magadan', 'Kazan', 'Khabarovsk', 'Volgograd', 'Vladivostok');
+CREATE TABLE sales_others PARTITION OF sales_list DEFAULT;
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Trump',    'Magadan',        1000, '2022-03-01');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Smirnoff', 'Smolensk',       500,  '2022-03-03');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Ford',     'St. Petersburg', 2000, '2022-03-05');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Ivanov',   'Moscow',         750,  '2022-03-04');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Deev',     'Voronezh',       250,  '2022-03-07');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Poirot',   'Kazan',          1000, '2022-03-01');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Ukhta',          1200, '2022-03-06');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Li',       'Vladivostok',    1150, '2022-03-09');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Ukhta',          1200, '2022-03-11');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Halder',   'Murmansk',       800,  '2022-03-02');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Muller',   'Bryansk',        650,  '2022-03-05');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Smith',    'Volgograd',      350,  '2022-03-10');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Gandi',    'Moscow',         150,  '2022-03-08');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Plato',    'Voronezh',       950,  '2022-03-05');
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk'),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd'));
+SELECT * FROM sales_list;
+ salesman_id | salesman_name |  sales_state   | sales_amount | sales_date 
+-------------+---------------+----------------+--------------+------------
+           2 | Smirnoff      | Smolensk       |          500 | 03-03-2022
+           5 | Deev          | Voronezh       |          250 | 03-07-2022
+          11 | Muller        | Bryansk        |          650 | 03-05-2022
+          14 | Plato         | Voronezh       |          950 | 03-05-2022
+           4 | Ivanov        | Moscow         |          750 | 03-04-2022
+           6 | Poirot        | Kazan          |         1000 | 03-01-2022
+          12 | Smith         | Volgograd      |          350 | 03-10-2022
+          13 | Gandi         | Moscow         |          150 | 03-08-2022
+           1 | Trump         | Magadan        |         1000 | 03-01-2022
+           8 | Li            | Vladivostok    |         1150 | 03-09-2022
+           3 | Ford          | St. Petersburg |         2000 | 03-05-2022
+           7 | May           | Ukhta          |         1200 | 03-06-2022
+           9 | May           | Ukhta          |         1200 | 03-11-2022
+          10 | Halder        | Murmansk       |          800 | 03-02-2022
+(14 rows)
+
+SELECT * FROM sales_west;
+ salesman_id | salesman_name | sales_state | sales_amount | sales_date 
+-------------+---------------+-------------+--------------+------------
+           2 | Smirnoff      | Smolensk    |          500 | 03-03-2022
+           5 | Deev          | Voronezh    |          250 | 03-07-2022
+          11 | Muller        | Bryansk     |          650 | 03-05-2022
+          14 | Plato         | Voronezh    |          950 | 03-05-2022
+(4 rows)
+
+SELECT * FROM sales_east;
+ salesman_id | salesman_name | sales_state | sales_amount | sales_date 
+-------------+---------------+-------------+--------------+------------
+           1 | Trump         | Magadan     |         1000 | 03-01-2022
+           8 | Li            | Vladivostok |         1150 | 03-09-2022
+(2 rows)
+
+SELECT * FROM sales_nord;
+ salesman_id | salesman_name |  sales_state   | sales_amount | sales_date 
+-------------+---------------+----------------+--------------+------------
+           3 | Ford          | St. Petersburg |         2000 | 03-05-2022
+           7 | May           | Ukhta          |         1200 | 03-06-2022
+           9 | May           | Ukhta          |         1200 | 03-11-2022
+          10 | Halder        | Murmansk       |          800 | 03-02-2022
+(4 rows)
+
+SELECT * FROM sales_central;
+ salesman_id | salesman_name | sales_state | sales_amount | sales_date 
+-------------+---------------+-------------+--------------+------------
+           4 | Ivanov        | Moscow      |          750 | 03-04-2022
+           6 | Poirot        | Kazan       |         1000 | 03-01-2022
+          12 | Smith         | Volgograd   |          350 | 03-10-2022
+          13 | Gandi         | Moscow      |          150 | 03-08-2022
+(4 rows)
+
+-- Use indexscan for test indexes after split partition
+SET enable_indexscan = ON;
+SET enable_seqscan = OFF;
+SELECT * FROM sales_central WHERE sales_state = 'Moscow';
+ salesman_id | salesman_name | sales_state | sales_amount | sales_date 
+-------------+---------------+-------------+--------------+------------
+           4 | Ivanov        | Moscow      |          750 | 03-04-2022
+          13 | Gandi         | Moscow      |          150 | 03-08-2022
+(2 rows)
+
+SELECT * FROM sales_list WHERE sales_state = 'Moscow';
+ salesman_id | salesman_name | sales_state | sales_amount | sales_date 
+-------------+---------------+-------------+--------------+------------
+           4 | Ivanov        | Moscow      |          750 | 03-04-2022
+          13 | Gandi         | Moscow      |          150 | 03-08-2022
+(2 rows)
+
+SELECT * FROM sales_list WHERE salesman_name = 'Ivanov';
+ salesman_id | salesman_name | sales_state | sales_amount | sales_date 
+-------------+---------------+-------------+--------------+------------
+           4 | Ivanov        | Moscow      |          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 (salesman_id INT, salesman_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;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+          13 | Gandi         |          377 | 01-09-2022
+(1 row)
+
+SELECT * FROM sales_feb2022_1decade;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+           2 | Smirnoff      |          500 | 02-09-2022
+           6 | Poirot        |          150 | 02-07-2022
+(2 rows)
+
+SELECT * FROM sales_mar2022_1decade;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+           7 | Li            |          175 | 03-08-2022
+(1 row)
+
+SELECT * FROM sales_apr2022_1decade;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+           5 | Deev          |          250 | 04-07-2022
+          11 | Trump         |          380 | 04-06-2022
+(2 rows)
+
+SELECT * FROM sales_others;
+ salesman_id | salesman_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 (salesman_id INT, salesman_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;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+          13 | Gandi         |          377 | 01-09-2022
+(1 row)
+
+SELECT * FROM sales_feb2022_1decade;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+           2 | Smirnoff      |          500 | 02-09-2022
+           6 | Poirot        |          150 | 02-07-2022
+(2 rows)
+
+SELECT * FROM sales_mar2022_1decade;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+           7 | Li            |          175 | 03-08-2022
+(1 row)
+
+SELECT * FROM sales_apr2022_1decade;
+ salesman_id | salesman_name | sales_amount | sales_date 
+-------------+---------------+--------------+------------
+           5 | Deev          |          250 | 04-07-2022
+          11 | Trump         |          380 | 04-06-2022
+(2 rows)
+
+SELECT * FROM sales_others;
+ salesman_id | salesman_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 (salesman_id INT, salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+ salesman_id | salesman_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;
+--
+DROP SCHEMA partition_split_schema;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 6f0236d207..117d78cb1b 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..e33e616230
--- /dev/null
+++ b/src/test/regress/sql/partition_split.sql
@@ -0,0 +1,792 @@
+--
+-- PARTITION_SPLIT
+-- Tests for "ALTER TABLE ... SPLIT PARTITION ..." command
+--
+
+CREATE SCHEMA partition_split_schema;
+SET search_path = partition_split_schema, public;
+
+--
+-- BY RANGE partitioning
+--
+
+--
+-- Test for error codes
+--
+CREATE TABLE sales_range (salesman_id int, salesman_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 contains 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" 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" 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" is not equals to upper bound of previous 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-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 equals 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'));
+
+DROP TABLE sales_range;
+DROP TABLE sales_others;
+
+--
+-- Add rows into partitioned table then split partition
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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 (salesman_id INT, salesman_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;
+
+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'));
+
+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 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 (salesman_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(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2021, 12, 7);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2021, 12, 8);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 1, 1);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 2, 4);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 1, 2);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager3', 2022, 2, 1);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 3, 3);
+INSERT INTO sales_date(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager2', 2022, 3, 4);
+INSERT INTO sales_date(salesman_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(salesman_name, sales_year, sales_month, sales_day) VALUES ('Manager1', 2022, 1, 10);
+INSERT INTO sales_date(salesman_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; using a index on partition key; check index after split
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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';
+
+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 test 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';
+SELECT * FROM pg_indexes WHERE tablename = 'sales_mar2022' and schemaname = 'partition_split_schema';
+SELECT * FROM pg_indexes WHERE tablename = 'sales_apr2022' and schemaname = 'partition_split_schema';
+SELECT * FROM pg_indexes WHERE tablename = 'sales_others' and schemaname = 'partition_split_schema';
+
+DROP TABLE sales_range CASCADE;
+
+--
+-- Test: some cases for split DEFAULT partition (different bounds)
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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 (exact 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:  any partition 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 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 (salesman_id INT, salesman_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 equals 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 salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30));
+INSERT INTO salesmans VALUES (1,  'Poirot');
+
+CREATE TABLE sales_range (
+salesman_id INT REFERENCES salesmans(salesman_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;
+
+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;
+SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_mar2022'::regclass::oid;
+SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_apr2022'::regclass::oid;
+
+-- ERROR:  new row for relation "sales_mar2022" violates check constraint "sales_range_salesman_id_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_salesman_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 salesmans CASCADE;
+
+--
+-- Test: split partition on partitioned table in case exists FOREIGN KEY reference from another table
+--
+CREATE TABLE salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+CREATE TABLE sales (salesman_id INT REFERENCES salesmans(salesman_id), sales_amount INT, sales_date DATE);
+
+CREATE TABLE salesmans01_10 PARTITION OF salesmans FOR VALUES FROM (1) TO (10);
+CREATE TABLE salesmans10_40 PARTITION OF salesmans FOR VALUES FROM (10) TO (40);
+
+INSERT INTO salesmans VALUES (1,  'Poirot');
+INSERT INTO salesmans VALUES (10, 'May');
+INSERT INTO salesmans VALUES (19, 'Ivanov');
+INSERT INTO salesmans VALUES (20, 'Smirnoff');
+INSERT INTO salesmans 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 salesmans01_10;
+SELECT * FROM salesmans10_40;
+
+ALTER TABLE salesmans SPLIT PARTITION salesmans10_40 INTO
+  (PARTITION salesmans10_20 FOR VALUES FROM (10) TO (20),
+   PARTITION salesmans20_30 FOR VALUES FROM (20) TO (30),
+   PARTITION salesmans30_40 FOR VALUES FROM (30) TO (40));
+
+SELECT * FROM salesmans01_10;
+SELECT * FROM salesmans10_20;
+SELECT * FROM salesmans20_30;
+SELECT * FROM salesmans30_40;
+
+-- ERROR:  insert or update on table "sales" violates foreign key constraint "sales_salesman_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 salesmans CASCADE;
+
+--
+-- Test: split partition of partitioned table with triggers
+--
+CREATE TABLE salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+
+CREATE TABLE salesmans01_10 PARTITION OF salesmans FOR VALUES FROM (1) TO (10);
+CREATE TABLE salesmans10_40 PARTITION OF salesmans FOR VALUES FROM (10) TO (40);
+
+INSERT INTO salesmans 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 salesmans_after_insert_statement_trigger
+    AFTER INSERT
+    ON salesmans
+    FOR EACH STATEMENT
+    EXECUTE PROCEDURE after_insert_row_trigger('salesmans');
+
+CREATE TRIGGER salesmans_after_insert_row_trigger
+    AFTER INSERT
+    ON salesmans
+    FOR EACH ROW
+    EXECUTE PROCEDURE after_insert_row_trigger('salesmans');
+
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salesmans VALUES (10, 'May');
+-- 1 trigger should fire here (row):
+INSERT INTO salesmans10_40 VALUES (19, 'Ivanov');
+
+ALTER TABLE salesmans SPLIT PARTITION salesmans10_40 INTO
+  (PARTITION salesmans10_20 FOR VALUES FROM (10) TO (20),
+   PARTITION salesmans20_30 FOR VALUES FROM (20) TO (30),
+   PARTITION salesmans30_40 FOR VALUES FROM (30) TO (40));
+
+-- 2 triggers should fire here (row + statement):
+INSERT INTO salesmans VALUES (20, 'Smirnoff');
+-- 1 trigger should fire here (row):
+INSERT INTO salesmans30_40 VALUES (30, 'Ford');
+
+SELECT * FROM salesmans01_10;
+SELECT * FROM salesmans10_20;
+SELECT * FROM salesmans20_30;
+SELECT * FROM salesmans30_40;
+
+DROP TABLE salesmans CASCADE;
+DROP FUNCTION after_insert_row_trigger();
+
+--
+-- Test: split partition with deleted columns
+--
+CREATE TABLE salesmans(salesman_id INT PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+
+CREATE TABLE salesmans01_10 PARTITION OF salesmans FOR VALUES FROM (1) TO (10);
+-- Create new partition with some deleted columns:
+CREATE TABLE salesmans10_40(d1 VARCHAR(30), salesman_id INT PRIMARY KEY, d2 INT, d3 DATE, salesman_name VARCHAR(30));
+
+INSERT INTO salesmans10_40 VALUES ('dummy value 1', 19, 100, now(), 'Ivanov');
+INSERT INTO salesmans10_40 VALUES ('dummy value 2', 20, 101, now(), 'Smirnoff');
+
+ALTER TABLE salesmans10_40 DROP COLUMN d1;
+ALTER TABLE salesmans10_40 DROP COLUMN d2;
+ALTER TABLE salesmans10_40 DROP COLUMN d3;
+
+ALTER TABLE salesmans ATTACH PARTITION salesmans10_40 FOR VALUES FROM (10) TO (40);
+
+INSERT INTO salesmans VALUES (1, 'Poirot');
+INSERT INTO salesmans VALUES (10, 'May');
+INSERT INTO salesmans VALUES (30, 'Ford');
+
+ALTER TABLE salesmans SPLIT PARTITION salesmans10_40 INTO
+  (PARTITION salesmans10_20 FOR VALUES FROM (10) TO (20),
+   PARTITION salesmans20_30 FOR VALUES FROM (20) TO (30),
+   PARTITION salesmans30_40 FOR VALUES FROM (30) TO (40));
+
+select * from salesmans01_10;
+select * from salesmans10_20;
+select * from salesmans20_30;
+select * from salesmans30_40;
+
+DROP TABLE salesmans CASCADE;
+
+--
+-- Test: split sub-partition
+--
+CREATE TABLE sales_range (salesman_id INT, salesman_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 (salesman_id INT, salesman_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
+(salesman_id INT,
+ salesman_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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Moscow', 'Voronezh', 'Smolensk', 'Bryansk', 'Magadan', 'Kazan', 'Khabarovsk', 'Volgograd', 'Vladivostok');
+CREATE TABLE sales_others PARTITION OF sales_list DEFAULT;
+
+-- ERROR:  partition "sales_east" would overlap partition "sales_nord"
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk'),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok', 'Murmansk'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd'));
+
+-- 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 ('Voronezh', 'Smolensk', 'Bryansk'),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Voronezh', 'Volgograd'));
+
+-- 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 ('Voronezh', 'Smolensk', 'Bryansk', NULL),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd'));
+
+DROP TABLE sales_list;
+
+--
+-- Test: two specific errors for BY LIST partitioning:
+--   * new partitions not has NULL value that split partition has.
+--   * new partitions not has a value that split partition has.
+--
+CREATE TABLE sales_list
+(salesman_id INT,
+ salesman_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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Moscow', 'Voronezh', 'Smolensk', 'Bryansk', 'Magadan', 'Kazan', 'Khabarovsk', 'Volgograd', 'Vladivostok', NULL);
+
+-- ERROR:  new partitions not have value NULL but split partition has
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk'),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd'));
+
+-- ERROR:  new partitions not have value 'Volgograd' but split partition has
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk'),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', NULL));
+
+DROP TABLE sales_list;
+
+--
+-- Test: BY LIST partitioning, SPLIT PARTITION with data
+--
+CREATE TABLE sales_list
+(salesman_id SERIAL,
+ salesman_name VARCHAR(30),
+ sales_state VARCHAR(20),
+ sales_amount INT,
+ sales_date DATE)
+PARTITION BY LIST (sales_state);
+
+CREATE INDEX sales_list_salesman_name_idx ON sales_list USING btree (salesman_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 ('Murmansk', 'St. Petersburg', 'Ukhta');
+CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Moscow', 'Voronezh', 'Smolensk', 'Bryansk', 'Magadan', 'Kazan', 'Khabarovsk', 'Volgograd', 'Vladivostok');
+CREATE TABLE sales_others PARTITION OF sales_list DEFAULT;
+
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Trump',    'Magadan',        1000, '2022-03-01');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Smirnoff', 'Smolensk',       500,  '2022-03-03');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Ford',     'St. Petersburg', 2000, '2022-03-05');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Ivanov',   'Moscow',         750,  '2022-03-04');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Deev',     'Voronezh',       250,  '2022-03-07');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Poirot',   'Kazan',          1000, '2022-03-01');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Ukhta',          1200, '2022-03-06');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Li',       'Vladivostok',    1150, '2022-03-09');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('May',      'Ukhta',          1200, '2022-03-11');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Halder',   'Murmansk',       800,  '2022-03-02');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Muller',   'Bryansk',        650,  '2022-03-05');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Smith',    'Volgograd',      350,  '2022-03-10');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Gandi',    'Moscow',         150,  '2022-03-08');
+INSERT INTO sales_list (salesman_name, sales_state, sales_amount, sales_date) VALUES ('Plato',    'Voronezh',       950,  '2022-03-05');
+
+ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
+  (PARTITION sales_west FOR VALUES IN ('Voronezh', 'Smolensk', 'Bryansk'),
+   PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+   PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd'));
+
+SELECT * FROM sales_list;
+SELECT * FROM sales_west;
+SELECT * FROM sales_east;
+SELECT * FROM sales_nord;
+SELECT * FROM sales_central;
+
+-- Use indexscan for test indexes after split partition
+SET enable_indexscan = ON;
+SET enable_seqscan = OFF;
+
+SELECT * FROM sales_central WHERE sales_state = 'Moscow';
+SELECT * FROM sales_list WHERE sales_state = 'Moscow';
+SELECT * FROM sales_list WHERE salesman_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 (salesman_id INT, salesman_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 (salesman_id INT, salesman_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 (salesman_id INT, salesman_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;
+
+--
+DROP SCHEMA partition_split_schema;
-- 
2.40.1.windows.1



  [text/plain] v23-0003-Documentation-for-ALTER-TABLE-SPLIT-PARTITION-ME.patch (9.2K, ../[email protected]/4-v23-0003-Documentation-for-ALTER-TABLE-SPLIT-PARTITION-ME.patch)
  download | inline diff:
From 383fdf8857a9d36bd5350c06d0dfbbe02499504c Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email protected]>
Date: Tue, 28 Mar 2023 10:50:19 +0300
Subject: [PATCH v23 3/4] Documentation for ALTER TABLE SPLIT PARTITION/MERGE
 PARTITIONS commands

---
 doc/src/sgml/ref/alter_table.sgml | 124 +++++++++++++++++++++++++++++-
 1 file changed, 121 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 9670671107..9fb31df250 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -37,6 +37,13 @@ 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>
 
 <phrase>where <replaceable class="parameter">action</replaceable> is one of:</phrase>
 
@@ -1106,14 +1113,99 @@ 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 split 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 DEFAULT partition, one of new partitions must be DEFAULT.
+      In case one of 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 contains 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>
+    </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 merge several partitions into 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 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 values lists of all partitions
+         <replaceable class="parameter">partition_name1</replaceable>,
+         <replaceable class="parameter">partition_name2</replaceable> [, ...] are
+         combined and form a 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>
+    </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>,
+   <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
@@ -1356,7 +1448,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 split partition, or the name of the new merged partition.
        </para>
       </listitem>
      </varlistentry>
@@ -1772,6 +1865,31 @@ 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 ('Voronezh', 'Smolensk', 'Bryansk'),
+    PARTITION sales_east FOR VALUES IN ('Magadan', 'Khabarovsk', 'Vladivostok'),
+    PARTITION sales_central FOR VALUES IN ('Moscow', 'Kazan', 'Volgograd'));
+</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>
-- 
2.40.1.windows.1



  [text/plain] v23-0004-SPLIT-PARTITION-optimization.patch (42.2K, ../[email protected]/5-v23-0004-SPLIT-PARTITION-optimization.patch)
  download | inline diff:
From d1dd3629e2817ee4d60ce12a0fde5ea9bdaecee0 Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email protected]>
Date: Thu, 26 Oct 2023 03:35:24 +0300
Subject: [PATCH v23 4/4] SPLIT PARTITION optimization

---
 src/backend/commands/tablecmds.c              | 686 +++++++++++++-----
 src/test/regress/expected/partition_split.out | 307 ++++++++
 src/test/regress/sql/partition_split.sql      | 152 ++++
 3 files changed, 978 insertions(+), 167 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b0d2e4af91..52e8ad2b35 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -20654,20 +20654,47 @@ typedef struct SplitPartitionContext
 	BulkInsertState bistate;	/* state of bulk inserts for partition */
 	TupleTableSlot *dstslot;	/* slot for insert row into partition */
 	Relation	partRel;		/* relation for partition */
+	SinglePartitionSpec *sps;	/* info about single partition (from SQL
+								 * command) */
 }			SplitPartitionContext;
 
+/*
+ * Struct with context of SPLIT PARTITION operation
+ */
+typedef struct SplitInfo
+{
+	PartitionCmd *cmd;			/* SPLIT PARTITION command info */
+
+	Relation	rel;			/* partitioned table */
+	Relation	splitRel;		/* split partition */
+
+	Oid			defaultPartOid; /* identifier of DEFAULT-partition in rel (if
+								 * exists) */
+	List	   *partContexts;	/* list of structs SplitPartitionContext (each
+								 * struct for each new partition) */
+	SplitPartitionContext *defaultPartCtx;	/* pointer to DEFAULT-partition in
+											 * partContexts list (if exists) */
+	EState	   *estate;			/* working state */
+}			SplitInfo;
 
 /*
- * createSplitPartitionContext: create context for partition and fill it
+ * createSplitPartitionContext: create context for partition
  */
 static SplitPartitionContext *
-createSplitPartitionContext(Relation partRel)
+createSplitPartitionContext(SinglePartitionSpec * sps)
 {
-	SplitPartitionContext *pc;
+	SplitPartitionContext *pc = (SplitPartitionContext *) palloc0(sizeof(SplitPartitionContext));
 
-	pc = (SplitPartitionContext *) palloc0(sizeof(SplitPartitionContext));
-	pc->partRel = partRel;
+	pc->sps = sps;
+	return pc;
+}
 
+/*
+ * fillSplitPartitionContext: fill partition context
+ */
+static void
+fillSplitPartitionContext(SplitPartitionContext * pc)
+{
 	/*
 	 * Prepare a BulkInsertState for table_tuple_insert. The FSM is empty, so
 	 * don't bother using it.
@@ -20678,67 +20705,66 @@ createSplitPartitionContext(Relation partRel)
 	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)
+deleteSplitPartitionContext(SplitPartitionContext * pc)
 {
-	ExecDropSingleTupleTableSlot(pc->dstslot);
-	FreeBulkInsertState(pc->bistate);
+	if (pc->dstslot)
+		ExecDropSingleTupleTableSlot(pc->dstslot);
+
+	if (pc->bistate)
+	{
+		/* The FSM is empty, so don't bother using it. */
+		int			ti_options = TABLE_INSERT_SKIP_FSM;
 
-	table_finish_bulk_insert(pc->partRel, ti_options);
+		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.
+ * createSplitInfo: create SPLIT PARTITION command context, contexts for new
+ *					partitions and generate constraints for them.
+ *					We need to use constraints for optimization.
  *
- * New partitions description:
- * partlist: list of pointers to SinglePartitionSpec structures.
- * newPartRels: list of Relation's.
+ * cmd: SPLIT PARTITION command info.
+ * rel: partitioned table.
+ * splitRel: split partition.
  * defaultPartOid: oid of DEFAULT partition, for table rel.
  */
-static void
-moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPartRels, Oid defaultPartOid)
+static SplitInfo *
+createSplitInfo(PartitionCmd *cmd, Relation rel, Relation splitRel,
+				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;
+	SplitInfo  *si;
+	ListCell   *listptr;
 
-	mycid = GetCurrentCommandId(true);
+	si = (SplitInfo *) palloc0(sizeof(SplitInfo));
 
-	estate = CreateExecutorState();
+	si->cmd = cmd;
+	si->rel = rel;
+	si->splitRel = splitRel;
 
-	forboth(listptr, partlist, listptr2, newPartRels)
+	si->defaultPartOid = defaultPartOid;
+	si->estate = CreateExecutorState();
+
+	/* Create context for each new partition and fill it. */
+	foreach(listptr, cmd->partlist)
 	{
 		SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
-
-		pc = createSplitPartitionContext((Relation) lfirst(listptr2));
+		SplitPartitionContext *pc = createSplitPartitionContext(sps);
 
 		if (sps->bound->is_default)
 		{
 			/* We should not create constraint for detached DEFAULT partition. */
-			defaultPartCtx = pc;
+			si->defaultPartCtx = pc;
 		}
 		else
 		{
@@ -20746,9 +20772,8 @@ moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPar
 
 			/* Build expression execution states for partition check quals. */
 			partConstraint = get_qual_from_partbound(rel, sps->bound);
-			partConstraint =
-				(List *) eval_const_expressions(NULL,
-												(Node *) partConstraint);
+			partConstraint = (List *) eval_const_expressions(NULL, (Node *) partConstraint);
+
 			/* Make boolean expression for ExecCheck(). */
 			partConstraint = list_make1(make_ands_explicit(partConstraint));
 
@@ -20756,11 +20781,10 @@ moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPar
 			 * Map the vars in the constraint expression from rel's attnos to
 			 * splitRel's.
 			 */
-			partConstraint = map_partition_varattnos(partConstraint,
-													 1, splitRel, rel);
+			partConstraint = map_partition_varattnos(partConstraint, 1, splitRel, rel);
 
 			pc->partqualstate =
-				ExecPrepareExpr((Expr *) linitial(partConstraint), estate);
+				ExecPrepareExpr((Expr *) linitial(partConstraint), si->estate);
 			Assert(pc->partqualstate != NULL);
 		}
 
@@ -20768,41 +20792,261 @@ moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPar
 		partContexts = lappend(partContexts, pc);
 	}
 
+	si->partContexts = partContexts;
+
+	return si;
+}
+
+/*
+ * deleteSplitInfo: delete SPLIT PARTITION command context
+ */
+static void
+deleteSplitInfo(SplitInfo * si)
+{
+	ListCell   *listptr;
+
+	FreeExecutorState(si->estate);
+
+	foreach(listptr, si->partContexts)
+		deleteSplitPartitionContext((SplitPartitionContext *) lfirst(listptr));
+
+	pfree(si);
+}
+
+/*
+ * checkNewPartitions: simple check of the new partitions.
+ *
+ * cmd: SPLIT PARTITION command info.
+ * splitRelOid: split partition Oid.
+ *
+ * Returns true if one of the new partitions has the same name as the split
+ * partition.
+ */
+static bool
+checkNewPartitions(PartitionCmd *cmd, Oid splitRelOid)
+{
+	Oid			namespaceId;
+	ListCell   *listptr;
+	bool		isSameName = false;
+	char		relname[NAMEDATALEN];
+
+	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.
+		 */
+		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)));
+	}
+
+	return isSameName;
+}
+
+/*
+ * createPartitionTable: create table for new partition with given name
+ * (newPartName) like table (modelRelName)
+ *
+ * Emulates command: CREATE TABLE <newPartName> (LIKE <modelRelName>
+ * INCLUDING ALL EXCLUDING INDEXES)
+ */
+static void
+createPartitionTable(RangeVar *newPartName, RangeVar *modelRelName,
+					 AlterTableUtilityContext *context)
+{
+	CreateStmt *createStmt;
+	TableLikeClause *tlc;
+	PlannedStmt *wrapper;
+
+	createStmt = makeNode(CreateStmt);
+	createStmt->relation = newPartName;
+	createStmt->tableElts = NIL;
+	createStmt->inhRelations = NIL;
+	createStmt->constraints = NIL;
+	createStmt->options = NIL;
+	createStmt->oncommit = ONCOMMIT_NOOP;
+	createStmt->tablespacename = NULL;
+	createStmt->if_not_exists = false;
+
+	tlc = makeNode(TableLikeClause);
+	tlc->relation = modelRelName;
+
+	/*
+	 * Indexes will be inherited on "attach new partitions" stage, after data
+	 * moving.
+	 */
+	tlc->options = CREATE_TABLE_LIKE_ALL & ~CREATE_TABLE_LIKE_INDEXES;
+	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);
+}
+
+/*
+ * createNewPartitions: simple check of the new partitions.
+ *
+ * si: SPLIT PARTITION command context.
+ * splitName: split partition name.
+ * pcWithAllRows: context of partition that contains all the rows of the split
+ * 				  partition or NULL if no such partition exists.
+ *
+ * Function returns name of split partition (and can change it in case of
+ * optimization with split partition renaming).
+ */
+static RangeVar *
+createNewPartitions(SplitInfo * si, RangeVar *splitName,
+					SplitPartitionContext * pcWithAllRows,
+					AlterTableUtilityContext *context)
+{
+	ListCell   *listptr;
+	Oid			splitRelOid;
+	RangeVar   *splitPartName = splitName;
+
+	splitRelOid = RelationGetRelid(si->splitRel);
+
+	foreach(listptr, si->partContexts)
+	{
+		SplitPartitionContext *pc = (SplitPartitionContext *) lfirst(listptr);
+
+		if (pc == pcWithAllRows)
+		{
+			/* Need to reuse splitRel for partition instead of creation. */
+
+			/*
+			 * We must bump the command counter to make the split partition
+			 * tuple visible for rename.
+			 */
+			CommandCounterIncrement();
+
+			/*
+			 * Rename split partition to new partition.
+			 */
+			RenameRelationInternal(splitRelOid, pc->sps->name->relname, false, false);
+			splitPartName = makeRangeVar(get_namespace_name(RelationGetNamespace(si->splitRel)),
+										 pc->sps->name->relname, -1);
+
+			/*
+			 * We must bump the command counter to make the split partition
+			 * tuple visible after rename.
+			 */
+			CommandCounterIncrement();
+
+			pc->partRel = si->splitRel;
+			/* No need to open relation : splitRel is already opened. */
+		}
+		else
+		{
+			createPartitionTable(pc->sps->name, splitPartName, context);
+
+			/* Open the new partition and acquire exclusive lock on it. */
+			pc->partRel = table_openrv(pc->sps->name, AccessExclusiveLock);
+		}
+	}
+
+	return splitPartName;
+}
+
+/*
+ * moveSplitTableRows: scan split partition (splitRel) of partitioned table
+ * (rel) and move rows into new partitions.
+ *
+ * si: SPLIT PARTITION command context.
+ */
+static void
+moveSplitTableRows(SplitInfo * si)
+{
+	/* The FSM is empty, so don't bother using it. */
+	int			ti_options = TABLE_INSERT_SKIP_FSM;
+	CommandId	mycid;
+	ListCell   *listptr;
+	TupleTableSlot *srcslot;
+	ExprContext *econtext;
+	TableScanDesc scan;
+	Snapshot	snapshot;
+	MemoryContext oldCxt;
+	TupleConversionMap *tuple_map;
+	SplitPartitionContext *pc = NULL;
+	bool		isOldDefaultPart = false;
+	SplitPartitionContext *defaultPartCtx = si->defaultPartCtx;
+
+	mycid = GetCurrentCommandId(true);
+
+	/* Prepare new partitions contexts for insert rows. */
+	foreach(listptr, si->partContexts)
+		fillSplitPartitionContext((SplitPartitionContext *) lfirst(listptr));
+
 	/*
 	 * 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))
+	if (!defaultPartCtx && OidIsValid(si->defaultPartOid))
 	{
 		/* Indicate that we allocate context for old DEFAULT partition */
 		isOldDefaultPart = true;
-		defaultPartCtx = createSplitPartitionContext(table_open(defaultPartOid, AccessExclusiveLock));
+		defaultPartCtx = createSplitPartitionContext(NULL);
+		defaultPartCtx->partRel = table_open(si->defaultPartOid, AccessExclusiveLock);
+		fillSplitPartitionContext(defaultPartCtx);
 	}
 
-	econtext = GetPerTupleExprContext(estate);
+	econtext = GetPerTupleExprContext(si->estate);
 
 	/* Create necessary tuple slot. */
-	srcslot = MakeSingleTupleTableSlot(RelationGetDescr(splitRel),
-									   table_slot_callbacks(splitRel));
+	srcslot = MakeSingleTupleTableSlot(RelationGetDescr(si->splitRel),
+									   table_slot_callbacks(si->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),
+	pc = (SplitPartitionContext *) lfirst(list_head(si->partContexts));
+	tuple_map = convert_tuples_by_name(RelationGetDescr(si->splitRel),
 									   RelationGetDescr(pc->partRel));
 
 	/* Scan through the rows. */
 	snapshot = RegisterSnapshot(GetLatestSnapshot());
-	scan = table_beginscan(splitRel, snapshot, 0, NULL);
+	scan = table_beginscan(si->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));
+	oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(si->estate));
 
 	while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot))
 	{
@@ -20815,7 +21059,7 @@ moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPar
 		econtext->ecxt_scantuple = srcslot;
 
 		/* Search partition for current slot srcslot. */
-		foreach(listptr, partContexts)
+		foreach(listptr, si->partContexts)
 		{
 			pc = (SplitPartitionContext *) lfirst(listptr);
 
@@ -20836,7 +21080,7 @@ moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPar
 				ereport(ERROR,
 						(errcode(ERRCODE_CHECK_VIOLATION),
 						 errmsg("can not find partition for split partition row"),
-						 errtable(splitRel)));
+						 errtable(si->splitRel)));
 		}
 
 		if (tuple_map)
@@ -20877,74 +21121,208 @@ moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPar
 
 	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;
+		Relation	defaultPartRel = defaultPartCtx->partRel;
 
-		deleteSplitPartitionContext(defaultPartCtx, ti_options);
+		deleteSplitPartitionContext(defaultPartCtx);
 		/* Keep the lock until commit. */
 		table_close(defaultPartRel, NoLock);
 	}
 }
 
 /*
- * createPartitionTable: create table for new partition with given name
- * (newPartName) like table (modelRelName)
+ * findNewPartForSlot: find partition that contains slot value.
  *
- * Emulates command: CREATE TABLE <newPartName> (LIKE <modelRelName>
- * INCLUDING ALL EXCLUDING INDEXES)
+ * si: SPLIT PARTITION context.
+ * checkPc: partition context for check slot value (can be NULL).
+ * slot: value to check.
  */
-static void
-createPartitionTable(RangeVar *newPartName, RangeVar *modelRelName,
-					 AlterTableUtilityContext *context)
+static SplitPartitionContext *
+findNewPartForSlot(SplitInfo * si, SplitPartitionContext * checkPc, TupleTableSlot *slot)
 {
-	CreateStmt *createStmt;
-	TableLikeClause *tlc;
-	PlannedStmt *wrapper;
+	ListCell   *listptr;
+	ExprContext *econtext;
+	MemoryContext oldCxt;
+	SplitPartitionContext *result = NULL;
 
-	createStmt = makeNode(CreateStmt);
-	createStmt->relation = newPartName;
-	createStmt->tableElts = NIL;
-	createStmt->inhRelations = NIL;
-	createStmt->constraints = NIL;
-	createStmt->options = NIL;
-	createStmt->oncommit = ONCOMMIT_NOOP;
-	createStmt->tablespacename = NULL;
-	createStmt->if_not_exists = false;
+	econtext = GetPerTupleExprContext(si->estate);
 
-	tlc = makeNode(TableLikeClause);
-	tlc->relation = modelRelName;
+	/* Make sure the tuple is fully deconstructed. */
+	slot_getallattrs(slot);
+
+	econtext->ecxt_scantuple = slot;
 
 	/*
-	 * Indexes will be inherited on "attach new partitions" stage, after data
-	 * moving.
+	 * Switch to per-tuple memory context and reset it after each check, so we
+	 * don't leak memory.
 	 */
-	tlc->options = CREATE_TABLE_LIKE_ALL & ~CREATE_TABLE_LIKE_INDEXES;
-	tlc->relationOid = InvalidOid;
-	createStmt->tableElts = lappend(createStmt->tableElts, tlc);
+	oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(si->estate));
 
-	/* 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;
+	if (checkPc)
+	{
+		if (ExecCheck(checkPc->partqualstate, econtext))
+		{
+			ResetExprContext(econtext);
+			result = checkPc;
+		}
+	}
+	else
+	{
+		/* Search partition for current slot srcslot. */
+		foreach(listptr, si->partContexts)
+		{
+			SplitPartitionContext *pc = (SplitPartitionContext *) lfirst(listptr);
 
-	ProcessUtility(wrapper,
-				   context->queryString,
-				   false,
-				   PROCESS_UTILITY_SUBCOMMAND,
-				   NULL,
-				   NULL,
-				   None_Receiver,
-				   NULL);
+			if (pc->partqualstate /* skip DEFAULT partition */ &&
+				ExecCheck(pc->partqualstate, econtext))
+			{
+				ResetExprContext(econtext);
+				result = pc;
+				break;
+			}
+			ResetExprContext(econtext);
+		}
+
+		/* We not found partition with borders but exists DEFAULT partition. */
+		if (!result && si->defaultPartCtx)
+			result = si->defaultPartCtx;
+
+		/*
+		 * "result" can be NULL here because can be spaces between of the new
+		 * partitions and rows from the spaces can be moved to the DEFAULT
+		 * partition of the partitioned table.
+		 */
+	}
+
+	MemoryContextSwitchTo(oldCxt);
+
+	return result;
+}
+
+/*
+ * findNewPartWithAllRows: find partition that contains all the rows of the
+ * split partition; returns partition context if partition was found.
+ *
+ * si: SPLIT PARTITION context.
+ */
+static SplitPartitionContext *
+findNewPartWithAllRows(SplitInfo * si)
+{
+	PartitionKey key = RelationGetPartitionKey(si->rel);
+	ListCell   *index;
+	int			partnatts;
+	SplitPartitionContext *result = NULL;
+	AttrMap    *map;
+	AttrNumber *partattrs;
+	int			i;
+
+	/* We can use optimization for BY RANGE partitioning only. */
+	if (key->strategy != PARTITION_STRATEGY_RANGE)
+		return NULL;
+
+	partnatts = get_partition_natts(key);
+
+	/*
+	 * Partition key contains columns of partitioned tables si->rel but index
+	 * contains columns of si->splitRel. So we need a map for convert
+	 * attributes numbers (si->rel) -> (si->splitRel).
+	 */
+	map = build_attrmap_by_name_if_req(RelationGetDescr(si->splitRel),
+									   RelationGetDescr(si->rel),
+									   false);
+	if (map)
+	{
+		/*
+		 * Columns order in a partitioned table and split partition is
+		 * different. So need to create a new array with attribute numbers.
+		 */
+		partattrs = palloc(sizeof(AttrNumber) * partnatts);
+		for (i = 0; i < partnatts; i++)
+		{
+			AttrNumber	attr_num = get_partition_col_attnum(key, i);
+
+			partattrs[i] = map->attnums[attr_num - 1];
+		}
+	}
+	else
+	{
+		/* We can use array of partition key. */
+		partattrs = key->partattrs;
+	}
+
+	/* Scan all indexes of split partition. */
+	foreach(index, RelationGetIndexList(si->splitRel))
+	{
+		Oid			thisIndexOid = lfirst_oid(index);
+		Relation	indexRel = index_open(thisIndexOid, AccessShareLock);
+
+		/*
+		 * Index should be valid, btree (for searching min/max) and contain
+		 * the same columns as partition key.
+		 */
+		if (indexRel->rd_index->indisvalid &&
+			indexRel->rd_rel->relam == BTREE_AM_OID &&
+			indexRel->rd_index->indnatts == partnatts)
+		{
+			for (i = 0; i < indexRel->rd_index->indnatts; i++)
+			{
+				if (indexRel->rd_index->indkey.values[i] != partattrs[i])
+					break;
+			}
+
+			/* Index found? */
+			if (i == indexRel->rd_index->indnatts)
+			{
+				IndexScanDesc indexScan;
+				TupleTableSlot *slot;
+
+				indexScan = index_beginscan(si->splitRel, indexRel, SnapshotAny, 0, 0);
+				do
+				{
+					SplitPartitionContext *pc;
+
+					/* Search a minimum index value. */
+					index_rescan(indexScan, NULL, 0, NULL, 0);
+					slot = table_slot_create(si->splitRel, NULL);
+					if (!index_getnext_slot(indexScan, ForwardScanDirection, slot))
+					{
+						ExecDropSingleTupleTableSlot(slot);
+						break;
+					}
+					/* Find partition context for minimum index value. */
+					pc = findNewPartForSlot(si, NULL, slot);
+					ExecDropSingleTupleTableSlot(slot);
+
+					/* Search a maximum index value. */
+					index_rescan(indexScan, NULL, 0, NULL, 0);
+					slot = table_slot_create(si->splitRel, NULL);
+					if (!index_getnext_slot(indexScan, BackwardScanDirection, slot))
+					{
+						ExecDropSingleTupleTableSlot(slot);
+						break;
+					}
+					/* Check partition context "pc" for maximum index value. */
+					result = findNewPartForSlot(si, pc, slot);
+					ExecDropSingleTupleTableSlot(slot);
+				} while (0);
+
+				index_endscan(indexScan);
+				index_close(indexRel, AccessShareLock);
+				goto done;
+			}
+		}
+		index_close(indexRel, AccessShareLock);
+	}
+
+done:
+	if (map)
+	{
+		pfree(partattrs);
+		free_attrmap(map);
+	}
+	return result;
 }
 
 /*
@@ -20956,16 +21334,14 @@ ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 {
 	Relation	splitRel;
 	Oid			splitRelOid;
-	char		relname[NAMEDATALEN];
-	Oid			namespaceId;
-	ListCell   *listptr,
-			   *listptr2;
+	ListCell   *listptr;
 	bool		isSameName = false;
 	char		tmpRelName[NAMEDATALEN];
-	List	   *newPartRels = NIL;
 	ObjectAddress object;
 	RangeVar   *splitPartName = cmd->name;
 	Oid			defaultPartOid;
+	SplitPartitionContext *pcWithAllRows;
+	SplitInfo  *si;
 
 	defaultPartOid = get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true));
 
@@ -20984,35 +21360,7 @@ ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	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.
-		 */
-		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)));
-	}
+	isSameName = checkNewPartitions(cmd, splitRelOid);
 
 	/* Detach split partition. */
 	RemoveInheritance(splitRel, rel, false);
@@ -21033,8 +21381,7 @@ ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		/* Rename partition. */
 		sprintf(tmpRelName, "split-%u-%X-tmp", RelationGetRelid(rel), MyProcPid);
 		RenameRelationInternal(splitRelOid, tmpRelName, false, false);
-		splitPartName = makeRangeVar(
-									 get_namespace_name(RelationGetNamespace(splitRel)),
+		splitPartName = makeRangeVar(get_namespace_name(RelationGetNamespace(splitRel)),
 									 tmpRelName, -1);
 
 		/*
@@ -21044,43 +21391,48 @@ ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		CommandCounterIncrement();
 	}
 
-	/* Create new partitions (like split partition), without indexes. */
-	foreach(listptr, cmd->partlist)
-	{
-		SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
-		Relation	newPartRel;
+	/* Create SPLIT PARTITION context. */
+	si = createSplitInfo(cmd, rel, splitRel, defaultPartOid);
 
-		createPartitionTable(sps->name, splitPartName, context);
+	/*
+	 * Optimization: if exist a new partition that contains all the rows of
+	 * the split partition then do not copy rows, rename the split partition.
+	 */
+	pcWithAllRows = findNewPartWithAllRows(si);
 
-		/* Open the new partition and acquire exclusive lock on it. */
-		newPartRel = table_openrv(sps->name, AccessExclusiveLock);
+	/* Create new partitions (like split partition), without indexes. */
+	splitPartName = createNewPartitions(si, splitPartName, pcWithAllRows, context);
 
-		newPartRels = lappend(newPartRels, newPartRel);
+	if (!pcWithAllRows)
+	{
+		/* Copy data from split partition to new partitions. */
+		moveSplitTableRows(si);
+		/* Keep the lock until commit. */
+		table_close(splitRel, NoLock);
 	}
 
-	/* 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)
+	foreach(listptr, si->partContexts)
 	{
-		SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
-		Relation	newPartRel = (Relation) lfirst(listptr2);
+		SplitPartitionContext *pc = (SplitPartitionContext *) lfirst(listptr);
 
 		/* wqueue = NULL: verification for each cloned constraint is not need. */
-		attachPartitionTable(NULL, rel, newPartRel, sps->bound);
+		attachPartitionTable(NULL, rel, pc->partRel, pc->sps->bound);
 		/* Keep the lock until commit. */
-		table_close(newPartRel, NoLock);
+		table_close(pc->partRel, NoLock);
 	}
 
-	/* Drop split partition. */
-	object.classId = RelationRelationId;
-	object.objectId = splitRelOid;
-	object.objectSubId = 0;
-	/* Probably DROP_CASCADE is not needed. */
-	performDeletion(&object, DROP_RESTRICT, 0);
+	if (!pcWithAllRows)
+	{
+		/* Drop split partition. */
+		object.classId = RelationRelationId;
+		object.objectId = splitRelOid;
+		object.objectSubId = 0;
+		/* Probably DROP_CASCADE is not needed. */
+		performDeletion(&object, DROP_RESTRICT, 0);
+	}
+
+	deleteSplitInfo(si);
 }
 
 /*
@@ -21226,8 +21578,8 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
 							RelationGetRelationName(pc->partRel))));
 
 		/*
-		 * Checking that two partitions have the same name was before,
-		 * in function transformPartitionCmdForMerge().
+		 * Checking that two partitions have the same name was before, in
+		 * function transformPartitionCmdForMerge().
 		 */
 		if (equal(name, cmd->name))
 			/* One new partition can have the same name as merged partition. */
diff --git a/src/test/regress/expected/partition_split.out b/src/test/regress/expected/partition_split.out
index 38736d3e67..4627a91e57 100644
--- a/src/test/regress/expected/partition_split.out
+++ b/src/test/regress/expected/partition_split.out
@@ -1314,4 +1314,311 @@ SELECT * FROM sales_others;
 
 DROP TABLE sales_range;
 --
+--
+-- Tests for SPLIT optimization (BY RANGE partitioning): if one of the new
+-- partitions contains all the rows of the split partition, then we can rename
+-- the split partition instead of creating a new partition and moving the rows.
+--
+-- 1. Optimization should be used.
+--
+CREATE TABLE test(name text, i int) PARTITION BY RANGE (i);
+CREATE TABLE test_1 PARTITION OF test FOR VALUES FROM (1) TO (10);
+CREATE TABLE test_def PARTITION OF test DEFAULT;
+CREATE INDEX idx_test_i ON test(i);
+INSERT INTO test(name, i) VALUES
+  ('a1', 1), ('a5', 5), ('a15', 15), ('a12', 12), ('a17', 17), ('a11', 11);
+-- should be rows 15, 12, 17, 11:
+SELECT i FROM test_def;
+ i  
+----
+ 15
+ 12
+ 17
+ 11
+(4 rows)
+
+SELECT 'test_def'::regclass::oid AS prev_oid \gset
+ALTER TABLE test SPLIT PARTITION test_def INTO 
+  (PARTITION test_def DEFAULT, PARTITION test_2 FOR VALUES FROM (11) TO (19));
+-- should be 't' (table "test_2" after SPLIT should be the same as table
+-- "test_def" before SPLIT):
+SELECT 'test_2'::regclass::oid=:prev_oid;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- should be rows 15, 12, 17, 11:
+SELECT i FROM test_2;
+ i  
+----
+ 15
+ 12
+ 17
+ 11
+(4 rows)
+
+-- should be 0 rows:
+SELECT i FROM test_def;
+ i 
+---
+(0 rows)
+
+-- should be 6 rows:
+SELECT * FROM test;
+ name | i  
+------+----
+ a1   |  1
+ a5   |  5
+ a15  | 15
+ a12  | 12
+ a17  | 17
+ a11  | 11
+(6 rows)
+
+DROP TABLE test CASCADE;
+--
+-- 2. Optimization cannot be used because not exists btree-index on the
+-- partition key (it is used to check the placement of rows in the partitions).
+--
+CREATE TABLE test(name text, i int) PARTITION BY RANGE (i);
+CREATE TABLE test_1 PARTITION OF test FOR VALUES FROM (1) TO (10);
+CREATE TABLE test_def PARTITION OF test DEFAULT;
+INSERT INTO test(name, i) VALUES
+  ('a1', 1), ('a5', 5), ('a15', 15), ('a12', 12), ('a17', 17), ('a11', 11);
+SELECT 'test_def'::regclass::oid AS prev_oid \gset
+ALTER TABLE test SPLIT PARTITION test_def INTO 
+  (PARTITION test_def DEFAULT, PARTITION test_2 FOR VALUES FROM (11) TO (19));
+-- should be 'f' (tables "test_2" and "test_def" should be different):
+SELECT 'test_2'::regclass::oid=:prev_oid;
+ ?column? 
+----------
+ f
+(1 row)
+
+-- should be rows 15, 12, 17, 11:
+SELECT i FROM test_2;
+ i  
+----
+ 15
+ 12
+ 17
+ 11
+(4 rows)
+
+-- should be 6 rows:
+SELECT * FROM test;
+ name | i  
+------+----
+ a1   |  1
+ a5   |  5
+ a15  | 15
+ a12  | 12
+ a17  | 17
+ a11  | 11
+(6 rows)
+
+DROP TABLE test CASCADE;
+--
+-- 3. Optimization cannot be used because rows should be moved into different
+-- partitions.
+--
+CREATE TABLE test(name text, i int) PARTITION BY RANGE (i);
+CREATE TABLE test_1 PARTITION OF test FOR VALUES FROM (1) TO (10);
+CREATE TABLE test_def PARTITION OF test DEFAULT;
+CREATE INDEX idx_test_i ON test(i);
+INSERT INTO test(name, i) VALUES
+  ('a1', 1), ('a5', 5), ('a15', 15), ('a12', 12), ('a27', 27), ('a11', 11);
+SELECT 'test_def'::regclass::oid AS prev_oid \gset
+ALTER TABLE test SPLIT PARTITION test_def INTO 
+  (PARTITION test_def DEFAULT, PARTITION test_2 FOR VALUES FROM (11) TO (19));
+-- should be 'f' (tables "test_2" and "test_def" should be different):
+SELECT 'test_2'::regclass::oid=:prev_oid;
+ ?column? 
+----------
+ f
+(1 row)
+
+-- should be rows 15, 12, 11:
+SELECT i FROM test_2;
+ i  
+----
+ 15
+ 12
+ 11
+(3 rows)
+
+-- should be 6 rows:
+SELECT * FROM test;
+ name | i  
+------+----
+ a1   |  1
+ a5   |  5
+ a15  | 15
+ a12  | 12
+ a11  | 11
+ a27  | 27
+(6 rows)
+
+DROP TABLE test CASCADE;
+--
+-- 4. Optimization should be used, DEFAUT partition renames to DEFAULT
+-- partition.
+--
+CREATE TABLE test(name text, i int) PARTITION BY RANGE (i);
+CREATE TABLE test_1 PARTITION OF test FOR VALUES FROM (1) TO (10);
+CREATE TABLE test_def PARTITION OF test DEFAULT;
+CREATE INDEX idx_test_i ON test(i);
+INSERT INTO test(name, i) VALUES
+  ('a1', 1), ('a5', 5), ('a25', 25), ('a22', 22), ('a27', 27), ('a21', 21);
+-- should be rows 25, 22, 27, 21:
+SELECT i FROM test_def;
+ i  
+----
+ 25
+ 22
+ 27
+ 21
+(4 rows)
+
+SELECT 'test_def'::regclass::oid AS prev_oid \gset
+ALTER TABLE test SPLIT PARTITION test_def INTO 
+  (PARTITION test_def DEFAULT, PARTITION test_2 FOR VALUES FROM (11) TO (19));
+-- should be 't' (table "test_def" after SPLIT should be the same as table
+-- "test_def" before SPLIT):
+SELECT 'test_def'::regclass::oid=:prev_oid;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- should be 0 rows:
+SELECT i FROM test_2;
+ i 
+---
+(0 rows)
+
+-- should be rows 25, 22, 27, 21:
+SELECT i FROM test_def;
+ i  
+----
+ 25
+ 22
+ 27
+ 21
+(4 rows)
+
+-- should be 6 rows:
+SELECT * FROM test;
+ name | i  
+------+----
+ a1   |  1
+ a5   |  5
+ a25  | 25
+ a22  | 22
+ a27  | 27
+ a21  | 21
+(6 rows)
+
+DROP TABLE test CASCADE;
+--
+-- 5. Optimization should be used, 2-column partition key + different columns
+-- order in partitions.
+--
+CREATE TABLE test_2colkey(s smallint, b bigint, t text) PARTITION BY RANGE (b, s);
+CREATE TABLE test_2colkey_1 PARTITION OF test_2colkey FOR VALUES FROM (1000000001, 1) TO (1000000100, 100);
+CREATE TABLE test_2colkey_def(i int, b bigint, s smallint, t text);
+ALTER TABLE test_2colkey_def DROP COLUMN i;
+ALTER TABLE test_2colkey ATTACH PARTITION test_2colkey_def DEFAULT;
+CREATE INDEX idx_test_2colkey_s_b ON test_2colkey(b, s);
+INSERT INTO test_2colkey (b, s, t) VALUES (1000000010, 3, 'value_10_3');
+INSERT INTO test_2colkey (b, s, t) VALUES (1000000120, 4, 'value_120_4');
+INSERT INTO test_2colkey (b, s, t) VALUES (1000000003, 5, 'value_3_5');
+INSERT INTO test_2colkey (b, s, t) VALUES (1000000124, 2, 'value_124_2');
+-- should be 4 rows:
+SELECT b, s FROM test_2colkey;
+     b      | s 
+------------+---
+ 1000000010 | 3
+ 1000000003 | 5
+ 1000000120 | 4
+ 1000000124 | 2
+(4 rows)
+
+-- should be 2 rows:
+SELECT b, s FROM test_2colkey_def;
+     b      | s 
+------------+---
+ 1000000120 | 4
+ 1000000124 | 2
+(2 rows)
+
+SELECT 'test_2colkey_def'::regclass::oid AS prev_oid \gset
+ALTER TABLE test_2colkey SPLIT PARTITION test_2colkey_def INTO
+  (PARTITION test_2colkey_2 FOR VALUES FROM (1000000101, 1) TO (1000000200, 100),
+   PARTITION test_2colkey_def DEFAULT);
+-- should be 't' (table "test_2colkey_2" after SPLIT should be the same as table
+-- "test_2colkey_def" before SPLIT):
+SELECT 'test_2colkey_2'::regclass::oid=:prev_oid;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- should be 2 rows:
+SELECT b, s FROM test_2colkey_2;
+     b      | s 
+------------+---
+ 1000000120 | 4
+ 1000000124 | 2
+(2 rows)
+
+-- should be 0 rows:
+SELECT b, s FROM test_2colkey_def;
+ b | s 
+---+---
+(0 rows)
+
+-- should be 6 rows:
+SELECT b, s FROM test_2colkey;
+     b      | s 
+------------+---
+ 1000000010 | 3
+ 1000000003 | 5
+ 1000000120 | 4
+ 1000000124 | 2
+(4 rows)
+
+--
+-- 5.1. Optimization cannot be used.
+--
+INSERT INTO test_2colkey (b, s, t) VALUES (1000000200, 1, 'value_200_1');
+SELECT 'test_2colkey_2'::regclass::oid AS prev_oid \gset
+ALTER TABLE test_2colkey SPLIT PARTITION test_2colkey_2 INTO
+  (PARTITION test_2colkey_2 FOR VALUES FROM (1000000101, 1) TO (1000000150, 100),
+   PARTITION test_2colkey_3 FOR VALUES FROM (1000000151, 1) TO (1000000200, 100));
+-- should be 'f' (optimization is not used):
+SELECT 'test_2colkey_2'::regclass::oid=:prev_oid;
+ ?column? 
+----------
+ f
+(1 row)
+
+-- should be 2 rows:
+SELECT b, s FROM test_2colkey_2;
+     b      | s 
+------------+---
+ 1000000120 | 4
+ 1000000124 | 2
+(2 rows)
+
+-- should be 1 row:
+SELECT b, s FROM test_2colkey_3;
+     b      | s 
+------------+---
+ 1000000200 | 1
+(1 row)
+
+DROP TABLE test_2colkey CASCADE;
+--
 DROP SCHEMA partition_split_schema;
diff --git a/src/test/regress/sql/partition_split.sql b/src/test/regress/sql/partition_split.sql
index e33e616230..0208c0158c 100644
--- a/src/test/regress/sql/partition_split.sql
+++ b/src/test/regress/sql/partition_split.sql
@@ -788,5 +788,157 @@ SELECT * FROM sales_others;
 
 DROP TABLE sales_range;
 
+--
+--
+-- Tests for SPLIT optimization (BY RANGE partitioning): if one of the new
+-- partitions contains all the rows of the split partition, then we can rename
+-- the split partition instead of creating a new partition and moving the rows.
+--
+-- 1. Optimization should be used.
+--
+CREATE TABLE test(name text, i int) PARTITION BY RANGE (i);
+CREATE TABLE test_1 PARTITION OF test FOR VALUES FROM (1) TO (10);
+CREATE TABLE test_def PARTITION OF test DEFAULT;
+CREATE INDEX idx_test_i ON test(i);
+INSERT INTO test(name, i) VALUES
+  ('a1', 1), ('a5', 5), ('a15', 15), ('a12', 12), ('a17', 17), ('a11', 11);
+-- should be rows 15, 12, 17, 11:
+SELECT i FROM test_def;
+SELECT 'test_def'::regclass::oid AS prev_oid \gset
+ALTER TABLE test SPLIT PARTITION test_def INTO 
+  (PARTITION test_def DEFAULT, PARTITION test_2 FOR VALUES FROM (11) TO (19));
+-- should be 't' (table "test_2" after SPLIT should be the same as table
+-- "test_def" before SPLIT):
+SELECT 'test_2'::regclass::oid=:prev_oid;
+-- should be rows 15, 12, 17, 11:
+SELECT i FROM test_2;
+-- should be 0 rows:
+SELECT i FROM test_def;
+-- should be 6 rows:
+SELECT * FROM test;
+DROP TABLE test CASCADE;
+--
+-- 2. Optimization cannot be used because not exists btree-index on the
+-- partition key (it is used to check the placement of rows in the partitions).
+--
+CREATE TABLE test(name text, i int) PARTITION BY RANGE (i);
+CREATE TABLE test_1 PARTITION OF test FOR VALUES FROM (1) TO (10);
+CREATE TABLE test_def PARTITION OF test DEFAULT;
+INSERT INTO test(name, i) VALUES
+  ('a1', 1), ('a5', 5), ('a15', 15), ('a12', 12), ('a17', 17), ('a11', 11);
+SELECT 'test_def'::regclass::oid AS prev_oid \gset
+ALTER TABLE test SPLIT PARTITION test_def INTO 
+  (PARTITION test_def DEFAULT, PARTITION test_2 FOR VALUES FROM (11) TO (19));
+-- should be 'f' (tables "test_2" and "test_def" should be different):
+SELECT 'test_2'::regclass::oid=:prev_oid;
+-- should be rows 15, 12, 17, 11:
+SELECT i FROM test_2;
+-- should be 6 rows:
+SELECT * FROM test;
+DROP TABLE test CASCADE;
+--
+-- 3. Optimization cannot be used because rows should be moved into different
+-- partitions.
+--
+CREATE TABLE test(name text, i int) PARTITION BY RANGE (i);
+CREATE TABLE test_1 PARTITION OF test FOR VALUES FROM (1) TO (10);
+CREATE TABLE test_def PARTITION OF test DEFAULT;
+CREATE INDEX idx_test_i ON test(i);
+INSERT INTO test(name, i) VALUES
+  ('a1', 1), ('a5', 5), ('a15', 15), ('a12', 12), ('a27', 27), ('a11', 11);
+SELECT 'test_def'::regclass::oid AS prev_oid \gset
+ALTER TABLE test SPLIT PARTITION test_def INTO 
+  (PARTITION test_def DEFAULT, PARTITION test_2 FOR VALUES FROM (11) TO (19));
+-- should be 'f' (tables "test_2" and "test_def" should be different):
+SELECT 'test_2'::regclass::oid=:prev_oid;
+-- should be rows 15, 12, 11:
+SELECT i FROM test_2;
+-- should be 6 rows:
+SELECT * FROM test;
+DROP TABLE test CASCADE;
+--
+-- 4. Optimization should be used, DEFAUT partition renames to DEFAULT
+-- partition.
+--
+CREATE TABLE test(name text, i int) PARTITION BY RANGE (i);
+CREATE TABLE test_1 PARTITION OF test FOR VALUES FROM (1) TO (10);
+CREATE TABLE test_def PARTITION OF test DEFAULT;
+CREATE INDEX idx_test_i ON test(i);
+INSERT INTO test(name, i) VALUES
+  ('a1', 1), ('a5', 5), ('a25', 25), ('a22', 22), ('a27', 27), ('a21', 21);
+-- should be rows 25, 22, 27, 21:
+SELECT i FROM test_def;
+SELECT 'test_def'::regclass::oid AS prev_oid \gset
+ALTER TABLE test SPLIT PARTITION test_def INTO 
+  (PARTITION test_def DEFAULT, PARTITION test_2 FOR VALUES FROM (11) TO (19));
+-- should be 't' (table "test_def" after SPLIT should be the same as table
+-- "test_def" before SPLIT):
+SELECT 'test_def'::regclass::oid=:prev_oid;
+-- should be 0 rows:
+SELECT i FROM test_2;
+-- should be rows 25, 22, 27, 21:
+SELECT i FROM test_def;
+-- should be 6 rows:
+SELECT * FROM test;
+DROP TABLE test CASCADE;
+--
+-- 5. Optimization should be used, 2-column partition key + different columns
+-- order in partitions.
+--
+CREATE TABLE test_2colkey(s smallint, b bigint, t text) PARTITION BY RANGE (b, s);
+CREATE TABLE test_2colkey_1 PARTITION OF test_2colkey FOR VALUES FROM (1000000001, 1) TO (1000000100, 100);
+CREATE TABLE test_2colkey_def(i int, b bigint, s smallint, t text);
+ALTER TABLE test_2colkey_def DROP COLUMN i;
+ALTER TABLE test_2colkey ATTACH PARTITION test_2colkey_def DEFAULT;
+CREATE INDEX idx_test_2colkey_s_b ON test_2colkey(b, s);
+
+INSERT INTO test_2colkey (b, s, t) VALUES (1000000010, 3, 'value_10_3');
+INSERT INTO test_2colkey (b, s, t) VALUES (1000000120, 4, 'value_120_4');
+INSERT INTO test_2colkey (b, s, t) VALUES (1000000003, 5, 'value_3_5');
+INSERT INTO test_2colkey (b, s, t) VALUES (1000000124, 2, 'value_124_2');
+
+-- should be 4 rows:
+SELECT b, s FROM test_2colkey;
+-- should be 2 rows:
+SELECT b, s FROM test_2colkey_def;
+
+SELECT 'test_2colkey_def'::regclass::oid AS prev_oid \gset
+
+ALTER TABLE test_2colkey SPLIT PARTITION test_2colkey_def INTO
+  (PARTITION test_2colkey_2 FOR VALUES FROM (1000000101, 1) TO (1000000200, 100),
+   PARTITION test_2colkey_def DEFAULT);
+
+-- should be 't' (table "test_2colkey_2" after SPLIT should be the same as table
+-- "test_2colkey_def" before SPLIT):
+SELECT 'test_2colkey_2'::regclass::oid=:prev_oid;
+
+-- should be 2 rows:
+SELECT b, s FROM test_2colkey_2;
+-- should be 0 rows:
+SELECT b, s FROM test_2colkey_def;
+-- should be 6 rows:
+SELECT b, s FROM test_2colkey;
+
+--
+-- 5.1. Optimization cannot be used.
+--
+INSERT INTO test_2colkey (b, s, t) VALUES (1000000200, 1, 'value_200_1');
+
+SELECT 'test_2colkey_2'::regclass::oid AS prev_oid \gset
+
+ALTER TABLE test_2colkey SPLIT PARTITION test_2colkey_2 INTO
+  (PARTITION test_2colkey_2 FOR VALUES FROM (1000000101, 1) TO (1000000150, 100),
+   PARTITION test_2colkey_3 FOR VALUES FROM (1000000151, 1) TO (1000000200, 100));
+
+-- should be 'f' (optimization is not used):
+SELECT 'test_2colkey_2'::regclass::oid=:prev_oid;
+
+-- should be 2 rows:
+SELECT b, s FROM test_2colkey_2;
+-- should be 1 row:
+SELECT b, s FROM test_2colkey_3;
+
+DROP TABLE test_2colkey CASCADE;
+
 --
 DROP SCHEMA partition_split_schema;
-- 
2.40.1.windows.1



view thread (173+ messages)  latest in thread

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected], [email protected], [email protected], [email protected]
  Subject: Re: Add SPLIT PARTITION/MERGE PARTITIONS commands
  In-Reply-To: <[email protected]>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox