Subject: Add SPLIT PARTITION/MERGE PARTITIONS commands

From: Dmitry Koval <[email-masked]>
Date: Tue, 31 May 2022 12:32:43 +0300

Hi, hackers!

There are not many commands in PostgreSQL for working with partitioned 
tables. This is an obstacle to their widespread use.
Adding SPLIT PARTITION/MERGE PARTITIONS operations can make easier to 
use partitioned tables in PostgreSQL.
(This is especially important when migrating projects from ORACLE DBMS.)

SPLIT PARTITION/MERGE PARTITIONS commands are supported for range 
partitioning (BY RANGE) and for list partitioning (BY LIST).
For hash partitioning (BY HASH) these operations are not supported.

=================
1 SPLIT PARTITION
=================
Command for split a single partition.

1.1 Syntax
----------

ALTER TABLE <name> SPLIT PARTITION <partition_name> INTO
(PARTITION <partition_name1> { FOR VALUES <partition_bound_spec> | 
DEFAULT },
   [ ... ]
   PARTITION <partition_nameN> { FOR VALUES <partition_bound_spec> | 
DEFAULT })

<partition_bound_spec>:
	IN ( <partition_bound_expr> [, ...] ) |
	FROM ( { <partition_bound_expr> | MINVALUE | MAXVALUE } [, ...] )
	TO ( { <partition_bound_expr> | MINVALUE | MAXVALUE } [, ...] )

1.2 Rules
---------

1.2.1 The <partition_name> partition should be split into two (or more) 
partitions.

1.2.2 New partitions should have different names (with existing 
partitions too).

1.2.3 Bounds of new partitions should not overlap with new and existing 
partitions.

1.2.4 In case split partition is DEFAULT partition, one of new 
partitions should be DEFAULT.

1.2.5 In case new partitions or existing partitions contains DEFAULT 
partition, new partitions <partition_name1>...<partition_nameN> can have 
any bounds inside split partition bound (can be spaces between 
partitions bounds).

1.2.6 In case partitioned table does not have DEFAULT partition, DEFAULT 
partition can be defined as one of new partition.

1.2.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 
<partition_name1>...<partition_nameN> should be equal to bound of split 
partition <partition_name>.

1.2.8 One of the new partitions <partition_name1>-<partition_nameN> can 
have the same name as split partition <partition_name> (this is suitable 
in case splitting a DEFAULT partition: we split it, but after splitting 
we have a partition with the same name).

1.2.9 Only simple (non-partitioned) partitions can be split.

1.3 Examples
------------

1.3.1 Example for range partitioning (BY RANGE):

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'));

1.3.2 Example for list partitioning (BY LIST):

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_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;

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'));

1.4 ToDo:
---------

1.4.1 Possibility to specify tablespace for each of the new partitions 
(currently new partitions are created in the same tablespace as split 
partition).
1.4.2 Possibility to use CONCURRENTLY mode that allows (during the SPLIT 
operation) not blocking partitions that are not splitting.

==================
2 MERGE PARTITIONS
==================
Command for merge several partitions into one partition.

2.1 Syntax
----------

ALTER TABLE <name> MERGE PARTITIONS (<partition_name1>, 
<partition_name2>[, ...]) INTO <new_partition_name>;

2.2 Rules
---------

2.2.1 The number of partitions that are merged into the new partition 
<new_partition_name> should be at least two.

2.2.2
If DEFAULT partition is not in the list of partitions <partition_name1>, 
<partition_name2>[, ...]:
   * for range partitioning (BY RANGE) is necessary that the ranges of 
the partitions <partition_name1>, <partition_name2>[, ...] 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 
<new_partition_name>.
   * for list partitioning (BY LIST) the values lists of all partitions 
<partition_name1>, <partition_name2>[, ...] are combined and form a list 
of values of partition <new_partition_name>.

If DEFAULT partition is in the list of partitions <partition_name1>, 
<partition_name2>[, ...]:
   * the partition <new_partition_name> will be the DEFAULT partition;
   * for both partitioning types (BY RANGE, BY LIST) the ranges and 
lists of values of the merged partitions can be any.

2.2.3 The new partition <new_partition_name> can have the same name as 
one of the merged partitions.

2.2.4 Only simple (non-partitioned) partitions can be merged.

2.3 Examples
------------

2.3.1 Example for range partitioning (BY RANGE):

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;

ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, 
sales_apr2022) INTO sales_feb_mar_apr2022;

2.3.2 Example for list partitioning (BY LIST):

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;

ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, 
sales_central) INTO sales_all;

2.4 ToDo:
---------

2.4.1 Possibility to specify tablespace for the new partition (currently 
new partition is created in the same tablespace as partitioned table).
2.4.2 Possibility to use CONCURRENTLY mode that allows (during the MERGE 
operation) not blocking partitions that are not merging.
2.4.3 New syntax for ALTER TABLE ... MERGE PARTITIONS command for range 
partitioning (BY RANGE):

ALTER TABLE <name> MERGE PARTITIONS <partition_name1> TO 
<partition_name2> INTO <new_partition_name>;

This command can merge all partitions between <partition_name1> and 
<partition_name2> into new partition <new_partition_name>.
This can be useful for this example cases: need to merge all one-month 
partitions into a year partition or need to merge all one-day partitions 
into a month partition.

Your opinions are very much welcome!

-- 
With best regards,
Dmitry Koval.

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2de0ebacec3..194a720388a 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -634,6 +634,11 @@ static List *GetParentedForeignKeyRefs(Relation partition);
 static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, char *compression);
 
+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);
 
 /* ----------------------------------------------------------------
  *		DefineRelation
@@ -4383,6 +4388,14 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = ShareUpdateExclusiveLock;
 				break;
 
+			case AT_SplitPartition:
+				cmd_lockmode = AccessExclusiveLock;
+				break;
+
+			case AT_MergePartitions:
+				cmd_lockmode = AccessExclusiveLock;
+				break;
+
 			case AT_CheckNotNull:
 
 				/*
@@ -4803,6 +4816,16 @@ 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 */
+			pass = AT_PASS_MISC;
+			break;
 		default:				/* oops */
 			elog(ERROR, "unrecognized alter table type: %d",
 				 (int) cmd->subtype);
@@ -5208,6 +5231,22 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 		case AT_DetachPartitionFinalize:
 			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);
+			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);
@@ -6195,6 +6234,10 @@ 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:
 			return "ALTER COLUMN ... ADD IDENTITY";
 		case AT_SetIdentity:
@@ -17649,6 +17692,35 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
 	}
 }
 
+/*
+ * attachPartTable: attach new partition to partitioned table
+ *
+ * rel: partitioned relation;
+ * attachRel: relation of attached partition;
+ * bound: bounds of attached relation.
+ */
+static void
+attachPartTable(List **wqueue, Relation rel, Relation attachRel, PartitionBoundSpec *bound)
+{
+	/* OK to create inheritance.  Rest of the checks performed there */
+	CreateInheritance(attachRel, rel);
+
+	/* Update the pg_class entry. */
+	StorePartitionBound(attachRel, rel, bound);
+
+	/* Ensure there exists a correct set of indexes in the partition. */
+	AttachPartitionEnsureIndexes(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
  *
@@ -17841,23 +17913,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);
-
-	/* Update the pg_class entry. */
-	StorePartitionBound(attachrel, rel, cmd->bound);
-
-	/* Ensure there exists a correct set of indexes in the partition. */
-	AttachPartitionEnsureIndexes(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. */
+	attachPartTable(wqueue, rel, attachrel, cmd->bound);
 
 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -19288,3 +19345,663 @@ GetAttributeCompression(Oid atttypid, char *compression)
 
 	return cmethod;
 }
+
+/*
+ * Struct with context of new partition for insert rows from splited partition
+ */
+typedef struct SplitPartContext
+{
+	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 */
+}			SplitPartContext;
+
+
+/*
+ * createSplitPartContext: create context for partition and fill it
+ */
+static SplitPartContext *
+createSplitPartContext(Relation partRel)
+{
+	SplitPartContext *pc;
+
+	pc = (SplitPartContext *) palloc0(sizeof(SplitPartContext));
+	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;
+}
+
+/*
+ * deleteSplitPartContext: delete context for partition
+ */
+static void
+deleteSplitPartContext(SplitPartContext * 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;
+	SplitPartContext *defaultPartCtx = NULL,
+			   *pc;
+	bool		isOldDefaultPart = false;
+
+	mycid = GetCurrentCommandId(true);
+
+	estate = CreateExecutorState();
+
+	forboth(listptr, partlist, listptr2, newPartRels)
+	{
+		SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
+
+		pc = createSplitPartContext((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 contect 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))
+	{
+		pc = createSplitPartContext(table_open(defaultPartOid, AccessExclusiveLock));
+
+		/* Indicate that we allocate context for old DEFAULT partition */
+		isOldDefaultPart = true;
+		defaultPartCtx = pc;
+	}
+
+	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 = (SplitPartContext *) 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))
+	{
+		SplitPartContext *pc;
+		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 = (SplitPartContext *) 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)
+		deleteSplitPartContext((SplitPartContext *) lfirst(listptr), ti_options);
+
+	/* Need to close table and free buffers for DEFAULT partition. */
+	if (isOldDefaultPart)
+	{
+		/* Keep the lock until commit. */
+		table_close(defaultPartCtx->partRel, NoLock);
+		deleteSplitPartContext(defaultPartCtx, ti_options);
+	}
+}
+
+/*
+ * 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);
+}
+
+/*
+ * 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. */
+		attachPartTable(NULL, rel, newPartRel, sps->bound);
+		/* Unlock new partition. */
+		table_close(newPartRel, NoLock);
+	}
+
+	/* Drop splitted 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
+ */
+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))));
+
+		if (!strcmp(name->relname, cmd->name->relname))
+			/* 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.
+	 */
+	attachPartTable(NULL, rel, newPartRel, cmd->bound);
+
+	/* Unlock and drop merged partitions. */
+	foreach(listptr, partContexts)
+	{
+		ObjectAddress object;
+		MergedPartContext *pc = (MergedPartContext *) lfirst(listptr);
+
+		/* Unlock partition. */
+		table_close(pc->partRel, NoLock);
+
+		object.classId = RelationRelationId;
+		object.objectId = RelationGetRelid(pc->partRel);
+		object.objectSubId = 0;
+		performDeletion(&object, DROP_RESTRICT, 0);
+
+		pfree(pc);
+	}
+
+	/* Rename new partition if it is need. */
+	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/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 51d630fa892..416ad65d528 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -5350,6 +5350,17 @@ _copyPartitionRangeDatum(const PartitionRangeDatum *from)
 	return newnode;
 }
 
+static SinglePartitionSpec *
+_copySinglePartitionSpec(const SinglePartitionSpec *from)
+{
+	SinglePartitionSpec *newnode = makeNode(SinglePartitionSpec);
+
+	COPY_NODE_FIELD(name);
+	COPY_NODE_FIELD(bound);
+
+	return newnode;
+}
+
 static PartitionCmd *
 _copyPartitionCmd(const PartitionCmd *from)
 {
@@ -5357,6 +5368,7 @@ _copyPartitionCmd(const PartitionCmd *from)
 
 	COPY_NODE_FIELD(name);
 	COPY_NODE_FIELD(bound);
+	COPY_NODE_FIELD(partlist);
 	COPY_SCALAR_FIELD(concurrent);
 
 	return newnode;
@@ -6560,6 +6572,9 @@ copyObjectImpl(const void *from)
 		case T_PartitionRangeDatum:
 			retval = _copyPartitionRangeDatum(from);
 			break;
+		case T_SinglePartitionSpec:
+			retval = _copySinglePartitionSpec(from);
+			break;
 		case T_PartitionCmd:
 			retval = _copyPartitionCmd(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index e747e1667d0..7e217f3e2a9 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -3443,11 +3443,21 @@ _equalPartitionRangeDatum(const PartitionRangeDatum *a, const PartitionRangeDatu
 	return true;
 }
 
+static bool
+_equalSinglePartitionSpec(const SinglePartitionSpec *a, const SinglePartitionSpec *b)
+{
+	COMPARE_NODE_FIELD(name);
+	COMPARE_NODE_FIELD(bound);
+
+	return true;
+}
+
 static bool
 _equalPartitionCmd(const PartitionCmd *a, const PartitionCmd *b)
 {
 	COMPARE_NODE_FIELD(name);
 	COMPARE_NODE_FIELD(bound);
+	COMPARE_NODE_FIELD(partlist);
 	COMPARE_SCALAR_FIELD(concurrent);
 
 	return true;
@@ -4363,6 +4373,9 @@ equal(const void *a, const void *b)
 		case T_PartitionRangeDatum:
 			retval = _equalPartitionRangeDatum(a, b);
 			break;
+		case T_SinglePartitionSpec:
+			retval = _equalSinglePartitionSpec(a, b);
+			break;
 		case T_PartitionCmd:
 			retval = _equalPartitionCmd(a, b);
 			break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 989db0dbece..d860f9021a1 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -273,6 +273,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;
@@ -644,6 +645,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
 
@@ -826,7 +829,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	ORDER ORDINALITY OTHERS OUT_P OUTER_P
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
 
-	PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH
+	PARALLEL PARAMETER PARSER PARTIAL PARTITION PARTITIONS PASSING PASSWORD PATH
 	PLACING PLAN PLANS POLICY
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
 	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
@@ -840,7 +843,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
+	SHARE SHOW SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SPLIT SQL_P STABLE STANDALONE_P
 	START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRING STRIP_P
 	SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P
 
@@ -2342,6 +2345,23 @@ alter_table_cmds:
 			| alter_table_cmds ',' alter_table_cmd	{ $$ = lappend($1, $3); }
 		;
 
+partitions_list:
+			SinglePartitionSpec							{ $$ = list_make1($1); }
+			| partitions_list ',' SinglePartitionSpec	{ $$ = lappend($1, $3); }
+		;
+
+SinglePartitionSpec:
+			PARTITION qualified_name PartitionBoundSpec
+				{
+					SinglePartitionSpec *n = makeNode(SinglePartitionSpec);
+
+					n->name = $2;
+					n->bound = $3;
+
+					$$ = n;
+				}
+		;
+
 partition_cmd:
 			/* ALTER TABLE <name> ATTACH PARTITION <table_name> FOR VALUES */
 			ATTACH PARTITION qualified_name PartitionBoundSpec
@@ -2352,6 +2372,7 @@ partition_cmd:
 					n->subtype = AT_AttachPartition;
 					cmd->name = $3;
 					cmd->bound = $4;
+					cmd->partlist = NULL;
 					cmd->concurrent = false;
 					n->def = (Node *) cmd;
 
@@ -2366,6 +2387,7 @@ partition_cmd:
 					n->subtype = AT_DetachPartition;
 					cmd->name = $3;
 					cmd->bound = NULL;
+					cmd->partlist = NULL;
 					cmd->concurrent = $4;
 					n->def = (Node *) cmd;
 
@@ -2379,6 +2401,35 @@ 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> 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
+				{
+					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;
@@ -2395,6 +2446,7 @@ index_partition_cmd:
 					n->subtype = AT_AttachPartition;
 					cmd->name = $3;
 					cmd->bound = NULL;
+					cmd->partlist = NULL;
 					cmd->concurrent = false;
 					n->def = (Node *) cmd;
 
@@ -17862,6 +17914,7 @@ unreserved_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PATH
@@ -17929,6 +17982,7 @@ unreserved_keyword:
 			| SIMPLE
 			| SKIP
 			| SNAPSHOT
+			| SPLIT
 			| SQL_P
 			| STABLE
 			| STANDALONE_P
@@ -18480,6 +18534,7 @@ bare_label_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PATH
@@ -18559,6 +18614,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 1a64a522798..28482597feb 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"
@@ -135,7 +137,7 @@ static void transformConstraintAttrs(CreateStmtContext *cxt,
 									 List *constraintList);
 static void transformColumnType(CreateStmtContext *cxt, ColumnDef *column);
 static void setSchemaName(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);
@@ -3273,6 +3275,116 @@ 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.
+ */
+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->partlist, cxt->pstate);
+}
+
+
+/*
+ * 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 (!strcmp(name->relname, name2->relname))
+				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
@@ -3536,7 +3648,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;
 				}
@@ -3544,6 +3656,24 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 				newcmds = lappend(newcmds, cmd);
 				break;
 
+			case AT_SplitPartition:
+			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")));
+
+					if (cmd->subtype == AT_SplitPartition)
+						transformPartitionCmdForSplit(&cxt, partcmd);
+					else
+						transformPartitionCmdForMerge(&cxt, partcmd);
+					newcmds = lappend(newcmds, cmd);
+					break;
+				}
+
 			default:
 
 				/*
@@ -3935,13 +4065,13 @@ setSchemaName(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;
 
@@ -3950,9 +4080,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:
 
@@ -3960,7 +4090,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 6b35111ebb6..9a10411f7c6 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -4998,3 +4998,843 @@ satisfies_hash_partition(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(rowHash % modulus == remainder);
 }
+
+/*
+ * check_partitions_not_overlap_range
+ *
+ * Checks that bounds of partitions (name, bound) and (prev_name, prev_bound)
+ * do not overlap. Lower bound of partition "prev_name" should be less or equal
+ * than lower bound of partition "name".
+ * In case defaultPart=true can be free space between bound and prev_bound.
+ */
+static void
+check_partitions_not_overlap_range(Relation parent,
+								   RangeVar *name,
+								   PartitionBoundSpec *bound,
+								   RangeVar *prev_name,
+								   PartitionBoundSpec *prev_bound,
+								   bool defaultPart,
+								   ParseState *pstate)
+{
+	PartitionKey key = RelationGetPartitionKey(parent);
+	PartitionRangeBound *lower;
+	PartitionRangeBound *prevupper;
+	int			cmpval;
+
+	Assert(key->strategy == PARTITION_STRATEGY_RANGE);
+
+	lower = make_one_partition_rbound(key, -1, bound->lowerdatums, true);
+	prevupper = make_one_partition_rbound(key, -1, prev_bound->upperdatums, false);
+
+	/* lower1=false for correct comparison lower and upper bounds */
+	cmpval = partition_rbound_cmp(key->partnatts,
+								  key->partsupfunc,
+								  key->partcollation,
+								  lower->datums, lower->kind,
+								  false, prevupper);
+	if ((!defaultPart && cmpval) || (defaultPart && cmpval < 0))
+	{
+		PartitionRangeDatum *datum = linitial(bound->lowerdatums);
+
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("lower bound of partition \"%s\" conflicts with upper bound of previous partition \"%s\"",
+						name->relname, prev_name->relname),
+				 parser_errposition(pstate, datum->location)));
+	}
+}
+
+/*
+ * check_partitions_not_overlap_list
+ *
+ * Checks that values of new partitions do not overlap.
+ * parts: array of SinglePartitionSpec structs
+ * nparts: size of array "parts".
+ */
+static void
+check_partitions_not_overlap_list(Relation parent,
+								  SinglePartitionSpec * *parts,
+								  int nparts,
+								  ParseState *pstate)
+{
+	PartitionKey key = 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)));
+			}
+		}
+	}
+}
+
+/*
+ * check_partition_bounds_for_split_range
+ *
+ * Checks that bounds of new partition is inside bounds of split partition
+ * (with splitPartOid). If first=true then lower bounds of partitions should
+ * be equals. If last=true then upper bounds of partitions should be equals.
+ * If defined spsPrev then function compares lower bound of spec with upper
+ * bound of spsPrev.
+ * defaultPart=true in case partitioned table has DEFAULT partition.
+ */
+static void
+check_partition_bounds_for_split_range(char *relname, Relation parent,
+									   PartitionBoundSpec *spec,
+									   Oid splitPartOid,
+									   bool first,
+									   bool last,
+									   bool defaultPart,
+									   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;
+	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)));
+	}
+
+	if (partdesc->nparts > 0)
+	{
+		int			offset;
+
+		Assert(boundinfo &&
+			   boundinfo->strategy == PARTITION_STRATEGY_RANGE &&
+			   (boundinfo->ndatums > 0 ||
+				partition_bound_has_default(boundinfo)));
+
+		/*
+		 * Test whether the new lower bound (which is treated inclusively as
+		 * part of the new partition) lies inside an existing partition, or in
+		 * a gap.
+		 *
+		 * If it's inside an existing partition, the bound at offset + 1 will
+		 * be the upper bound of that partition, and its index will be >= 0.
+		 *
+		 * If it's in a gap, the bound at offset + 1 will be the lower bound
+		 * of the next partition, and its index will be -1. This is also true
+		 * if there is no next partition, since the index array is initialised
+		 * with an extra -1 at the end.
+		 */
+		offset = partition_range_bsearch(key->partnatts,
+										 key->partsupfunc,
+										 key->partcollation,
+										 boundinfo, lower,
+										 &cmpval);
+
+		if (boundinfo->indexes[offset + 1] < 0)
+		{
+			PartitionRangeDatum *datum = list_nth(spec->lowerdatums, Abs(cmpval) - 1);
+
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("bounds of partition \"%s\" are outside bounds of split partition",
+							relname),
+					 parser_errposition(pstate, datum->location)));
+		}
+		else
+		{
+			/*
+			 * The new partition overlaps with the existing partition between
+			 * offset and offset + 1.
+			 */
+			PartitionRangeDatum *datum;
+
+			/*
+			 * Point to problematic key in the lower datums list; if we have
+			 * equality, point to the first one.
+			 */
+			datum = cmpval == 0 ? linitial(spec->lowerdatums) :
+				list_nth(spec->lowerdatums, Abs(cmpval) - 1);
+			overlap_location = datum->location;
+			with = boundinfo->indexes[offset + 1];
+			if (partdesc->oids[with] != splitPartOid)
+				overlap = true;
+			else if (first || last)
+			{
+				/*
+				 * Lower bound of first partition should be equals to lower
+				 * bound of split partition. Upper bound of last partition
+				 * should be equals to upper bound of split partition.
+				 *
+				 * Need to do exact check of this condition if not exists
+				 * DEFAULT partition. Otherwise can be spaces between
+				 * partitions.
+				 */
+				Datum	   *datums;
+				PartitionRangeDatumKind *kind;
+				int			curoffset = offset + (last ? 1 : 0);
+
+				datums = boundinfo->datums[curoffset];
+				kind = boundinfo->kind[curoffset];
+
+				cmpval = partition_rbound_cmp(key->partnatts,
+											  key->partsupfunc,
+											  key->partcollation,
+											  datums, kind,
+											  first,
+											  first ? lower : upper);
+				if ((!defaultPart && cmpval) ||
+				/* Is partition bound outside of split partition bound? */
+					(defaultPart && first && cmpval > 0) ||
+					(defaultPart && last && cmpval < 0))
+				{
+					PartitionRangeDatum *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 not equals to %s bound of split partition",
+									first ? "lower" : "upper",
+									relname,
+									first ? "lower" : "upper"),
+							 parser_errposition(pstate, datum->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)));
+	}
+}
+
+/*
+ * check_partition_bounds_for_split_list
+ *
+ * Checks that bounds of new partition is inside bounds of split partition
+ * (with splitPartOid).
+ */
+static void
+check_partition_bounds_for_split_list(char *relname, Relation parent,
+									  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);
+
+	/* Should be exists at least one partition for split: */
+	Assert(partdesc->nparts > 0);
+
+	Assert(boundinfo &&
+		   boundinfo->strategy == PARTITION_STRATEGY_LIST &&
+		   (boundinfo->ndatums > 0 ||
+			partition_bound_accepts_nulls(boundinfo) ||
+			partition_bound_has_default(boundinfo)));
+
+	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 returns "true" in case list of new partitions (partlist) contains
+ * value "value" and "false" if not contains.
+ */
+static bool
+find_value_in_new_partitions(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
+ *
+ * Checks that all values of split partition (partOid) contains in new
+ * partitions (partlist).
+ */
+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(&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 && found; 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(&key->partsupfunc[0],
+											  key->partcollation, parts, nparts, datum, false))
+				found = false;
+		}
+	}
+
+	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
+ *
+ * Need to check 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.
+ */
+void
+check_partitions_for_split(Relation parent,
+						   Oid splitPartOid,
+						   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 splitted")));
+			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(sps->name->relname,
+													   parent, sps->bound, splitPartOid, first, last,
+													   existsDefaultPart, pstate);
+			}
+			else
+				check_partition_bounds_for_split_list(sps->name->relname,
+													  parent, sps->bound, splitPartOid, pstate);
+		}
+
+		/* Ranges of new partitions should not overlap. */
+		if (strategy == PARTITION_STRATEGY_RANGE && spsPrev)
+			check_partitions_not_overlap_range(parent, sps->name, sps->bound,
+											   spsPrev->name, spsPrev->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 (!strcmp(sps->name->relname, sps2->name->relname))
+				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);
+}
+
+/*
+ * 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 bound of merged partition "spec" by using bounds of partitions
+ * with Oids "partOids" and names "partNames".
+ */
+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_partitions_not_overlap_range(parent,
+													   (RangeVar *) list_nth(partNames, index),
+													   (PartitionBoundSpec *) list_nth(bounds, index),
+													   (RangeVar *) list_nth(partNames, prev_index),
+													   (PartitionBoundSpec *) list_nth(bounds, prev_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/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index c3937a60fd3..1622c4f7c32 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -12869,3 +12869,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/nodes.h b/src/include/nodes/nodes.h
index b3b407579b0..7ef275569c1 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -504,6 +504,7 @@ typedef enum NodeTag
 	T_PartitionSpec,
 	T_PartitionBoundSpec,
 	T_PartitionRangeDatum,
+	T_SinglePartitionSpec,
 	T_PartitionCmd,
 	T_VacuumRelation,
 	T_PublicationObjSpec,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 73f635b4553..27556c7c059 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -886,6 +886,18 @@ typedef struct PartitionRangeDatum
 	int			location;		/* token location, or -1 if unknown */
 } PartitionRangeDatum;
 
+/*
+ * PartitionDesc - info about single partition for ALTER TABLE SPLIT PARTITION command
+ * FIXME: need to place this struct into PartitionCmd instead of pair (name, bound)
+ */
+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
  */
@@ -894,6 +906,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;
 
@@ -2291,6 +2304,8 @@ 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 */
 	AT_DropIdentity,			/* DROP IDENTITY */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 8a2ab405a28..b58530d0114 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -333,6 +333,7 @@ PG_KEYWORD("parameter", PARAMETER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("parser", PARSER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("partitions", PARTITIONS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -415,6 +416,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 b1e3f1b8400..3630c0eaef6 100644
--- a/src/include/partitioning/partbounds.h
+++ b/src/include/partitioning/partbounds.h
@@ -143,4 +143,14 @@ 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,
+									   List *partlist,
+									   ParseState *pstate);
+extern void calculate_partition_bound_for_merge(Relation parent,
+												List *partNames,
+												List *partOids,
+												PartitionBoundSpec *spec,
+												ParseState *pstate);
+
 #endif							/* PARTBOUNDS_H */
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 7d489718a35..ed13a43f53d 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -44,4 +44,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-merge.out b/src/test/isolation/expected/partition-split-merge.out
new file mode 100644
index 00000000000..34df1345b2b
--- /dev/null
+++ b/src/test/isolation/expected/partition-split-merge.out
@@ -0,0 +1,36 @@
+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, 'text1'); <waiting ...>
+step s1c: COMMIT;
+step s2i: <... completed>
+step s2c: COMMIT;
+step s2s: SELECT * FROM tpart;
+i|t    
+-+-----
+1|text1
+(1 row)
+
+
+starting permutation: s2b s2i s2c s1b s1merg s2b s2u s1c s2c s2s
+step s2b: BEGIN;
+step s2i: INSERT INTO tpart VALUES (1, 'text1');
+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 = 'text1modif' where i = 1; <waiting ...>
+step s1c: COMMIT;
+step s2u: <... completed>
+step s2c: COMMIT;
+step s2s: SELECT * FROM tpart;
+i|t         
+-+----------
+1|text1modif
+(1 row)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 529a4cbd4da..22e81b01a4b 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -101,6 +101,7 @@ test: partition-key-update-1
 test: partition-key-update-2
 test: partition-key-update-3
 test: partition-key-update-4
+test: partition-split-merge
 test: plpgsql-toast
 test: cluster-conflict
 test: cluster-conflict-partition
diff --git a/src/test/isolation/specs/partition-split-merge.spec b/src/test/isolation/specs/partition-split-merge.spec
new file mode 100644
index 00000000000..e10c7c607a6
--- /dev/null
+++ b/src/test/isolation/specs/partition-split-merge.spec
@@ -0,0 +1,43 @@
+# Verify that SPLIT and MERGE operations 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;
+}
+
+teardown
+{
+  DROP TABLE tpart;
+}
+
+session s1
+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 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 s2i	{ INSERT INTO tpart VALUES (1, 'text1'); }
+step s2u	{ UPDATE tpart SET t = 'text1modif' where i = 1; }
+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
+
+# 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
diff --git a/src/test/regress/expected/partition_merge.out b/src/test/regress/expected/partition_merge.out
new file mode 100644
index 00000000000..c783c4ad098
--- /dev/null
+++ b/src/test/regress/expected/partition_merge.out
@@ -0,0 +1,715 @@
+--
+-- 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;
+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'));
+-- 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/expected/partition_split.out b/src/test/regress/expected/partition_split.out
new file mode 100644
index 00000000000..a0e79a711ba
--- /dev/null
+++ b/src/test/regress/expected/partition_split.out
@@ -0,0 +1,1425 @@
+--
+-- 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 splitted 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:  bounds of partition "sales_feb2022" are outside bounds 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:  new partition "sales_feb2022" would overlap with another (not split) partition "sales_jan2022"
+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 not equals to 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 not equals to upper bound of split partition
+LINE 4: ... sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-06-0...
+                                                             ^
+-- ERROR:  new partition "sales_feb2022" would overlap with another (not split) partition "sales_jan2022"
+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:  new partition "sales_feb2022" would overlap with another (not split) partition "sales_jan2022"
+LINE 2:   (PARTITION sales_feb2022 FOR VALUES FROM ('2022-01-01') TO...
+                                                    ^
+-- 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 witch identity column
+-- If split partition column is identity-column, columns of new partitions are identity-columns too.
+--
+CREATE TABLE salesmans(salesman_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+CREATE TABLE salesmans1_2 PARTITION OF salesmans FOR VALUES FROM (1) TO (2);
+-- Create new partition with identity-column:
+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);
+INSERT INTO salesmans (salesman_name) VALUES ('Poirot');
+INSERT INTO salesmans (salesman_name) VALUES ('Ivanov');
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans'::regclass::oid;
+    attname    | attidentity | attgenerated 
+---------------+-------------+--------------
+ salesman_id   | a           | 
+ salesman_name |             | 
+(2 rows)
+
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans1_2'::regclass::oid;
+    attname    | attidentity | attgenerated 
+---------------+-------------+--------------
+ salesman_id   |             | 
+ salesman_name |             | 
+(2 rows)
+
+-- Split partition has identity-column:
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans2_5'::regclass::oid;
+    attname    | attidentity | attgenerated 
+---------------+-------------+--------------
+ salesman_id   | a           | 
+ salesman_name |             | 
+(2 rows)
+
+ALTER TABLE salesmans SPLIT PARTITION salesmans2_5 INTO
+  (PARTITION salesmans2_3 FOR VALUES FROM (2) TO (3),
+   PARTITION salesmans3_4 FOR VALUES FROM (3) TO (4),
+   PARTITION salesmans4_5 FOR VALUES FROM (4) TO (5));
+INSERT INTO salesmans (salesman_name) VALUES ('May');
+INSERT INTO salesmans (salesman_name) VALUES ('Ford');
+SELECT * FROM salesmans1_2;
+ salesman_id | salesman_name 
+-------------+---------------
+           1 | Poirot
+(1 row)
+
+SELECT * FROM salesmans2_3;
+ salesman_id | salesman_name 
+-------------+---------------
+           2 | Ivanov
+(1 row)
+
+SELECT * FROM salesmans3_4;
+ salesman_id | salesman_name 
+-------------+---------------
+           3 | May
+(1 row)
+
+SELECT * FROM salesmans4_5;
+ salesman_id | salesman_name 
+-------------+---------------
+           4 | Ford
+(1 row)
+
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans'::regclass::oid;
+    attname    | attidentity | attgenerated 
+---------------+-------------+--------------
+ salesman_id   | a           | 
+ salesman_name |             | 
+(2 rows)
+
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans1_2'::regclass::oid;
+    attname    | attidentity | attgenerated 
+---------------+-------------+--------------
+ salesman_id   |             | 
+ salesman_name |             | 
+(2 rows)
+
+-- New partitions have identity-columns:
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans2_3'::regclass::oid;
+    attname    | attidentity | attgenerated 
+---------------+-------------+--------------
+ salesman_id   | a           | 
+ salesman_name |             | 
+(2 rows)
+
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans3_4'::regclass::oid;
+    attname    | attidentity | attgenerated 
+---------------+-------------+--------------
+ salesman_id   | a           | 
+ salesman_name |             | 
+(2 rows)
+
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans4_5'::regclass::oid;
+    attname    | attidentity | attgenerated 
+---------------+-------------+--------------
+ salesman_id   | a           | 
+ salesman_name |             | 
+(2 rows)
+
+DROP TABLE salesmans CASCADE;
+--
+-- 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 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_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 103e11483d2..c6f974d7b5c 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -127,7 +127,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
+test: partition_merge partition_split partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats
 
 # event_trigger cannot run concurrently with any test that runs DDL
 # oidjoins is read-only, though, and should run late for best coverage
diff --git a/src/test/regress/sql/partition_merge.sql b/src/test/regress/sql/partition_merge.sql
new file mode 100644
index 00000000000..5a724ce65db
--- /dev/null
+++ b/src/test/regress/sql/partition_merge.sql
@@ -0,0 +1,411 @@
+--
+-- 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;
+
+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'));
+
+-- 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/test/regress/sql/partition_split.sql b/src/test/regress/sql/partition_split.sql
new file mode 100644
index 00000000000..75173e2752a
--- /dev/null
+++ b/src/test/regress/sql/partition_split.sql
@@ -0,0 +1,839 @@
+--
+-- 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 splitted 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:  bounds of partition "sales_feb2022" are outside bounds 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 not equals to 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:  new partition "sales_feb2022" would overlap with another (not split) partition "sales_jan2022"
+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_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 witch identity column
+-- If split partition column is identity-column, columns of new partitions are identity-columns too.
+--
+CREATE TABLE salesmans(salesman_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, salesman_name VARCHAR(30)) PARTITION BY RANGE (salesman_id);
+
+CREATE TABLE salesmans1_2 PARTITION OF salesmans FOR VALUES FROM (1) TO (2);
+-- Create new partition with identity-column:
+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);
+
+INSERT INTO salesmans (salesman_name) VALUES ('Poirot');
+INSERT INTO salesmans (salesman_name) VALUES ('Ivanov');
+
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans'::regclass::oid;
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans1_2'::regclass::oid;
+-- Split partition has identity-column:
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans2_5'::regclass::oid;
+
+ALTER TABLE salesmans SPLIT PARTITION salesmans2_5 INTO
+  (PARTITION salesmans2_3 FOR VALUES FROM (2) TO (3),
+   PARTITION salesmans3_4 FOR VALUES FROM (3) TO (4),
+   PARTITION salesmans4_5 FOR VALUES FROM (4) TO (5));
+
+INSERT INTO salesmans (salesman_name) VALUES ('May');
+INSERT INTO salesmans (salesman_name) VALUES ('Ford');
+
+SELECT * FROM salesmans1_2;
+SELECT * FROM salesmans2_3;
+SELECT * FROM salesmans3_4;
+SELECT * FROM salesmans4_5;
+
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans'::regclass::oid;
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans1_2'::regclass::oid;
+-- New partitions have identity-columns:
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans2_3'::regclass::oid;
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans3_4'::regclass::oid;
+SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans4_5'::regclass::oid;
+
+DROP TABLE salesmans CASCADE;
+
+--
+-- 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 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_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;

---

From: Matthias van de Meent <[email-masked]>
Date: Tue, 31 May 2022 12:30:22 +0200

> use partitioned tables in PostgreSQL.

That is quite a nice and useful feature to have.

> For hash partitioning (BY HASH) these operations are not supported.

Just out of curiosity, why is SPLIT / MERGE support not included for
HASH partitions? Because sibling partitions can have a different
modulus, you should be able to e.g. split a partition with (modulus,
remainder) of (3, 1) into two partitions with (mod, rem) of (6, 1) and
(6, 4) respectively, with the reverse being true for merge operations,
right?

Kind regards,

Matthias van de Meent

---

From: Laurenz Albe <[email-masked]>
Date: Tue, 31 May 2022 13:02:27 +0200

> For hash partitioning (BY HASH) these operations are not supported.

+1 on the general idea.

At least, it will makes these operations simpler, but probably also less
invasive (no need to detach the affected partitions).

I didn't read the patch, but what lock level does that place on the
partitioned table?  Anything more than ACCESS SHARE?

Yours,
Laurenz Albe

---

From: Dmitry Koval <[email-masked]>
Date: Tue, 31 May 2022 22:43:16 +0300

>(6, 4) respectively, with the reverse being true for merge operations,

You are right, SPLIT/MERGE operations can be added for HASH-partitioning 
in the future. But HASH-partitioning is rarer than RANGE- and 
LIST-partitioning and I decided to skip it in the first step.
Maybe community will say that SPLIT/MERGE commands are not needed... (At 
first step I would like to make sure that it is no true)

P.S. I attached patch with 1-line warning fix (for cfbot).
-- 
With best regards,
Dmitry Koval

Postgres Professional: http://postgrespro.com

 

 				/*
 
+/*

 	/*
 
 	return cmethod;
 }
+
+/*

 	COPY_NODE_FIELD(name);
 	COPY_NODE_FIELD(bound);
 
 	return newnode;
 
+static bool
 
 	return true;

 

+partitions_list:

 

+/*

 
 				/*
 
 /*
  * transformPartitionCmd

index 6b35111ebb6..d7873421d04 100644
 
+	PartitionKey key PG_USED_FOR_ASSERTS_ONLY = RelationGetPartitionKey(parent);
 
 	return buf->data;
 }
+
+/*
 
+/*

---

From: Dmitry Koval <[email-masked]>
Date: Tue, 31 May 2022 23:22:32 +0300

> partitioned table?  Anything more than ACCESS SHARE?

Current patch locks a partitioned table with ACCESS EXCLUSIVE lock. 
Unfortunately only this lock guarantees that other session can not work 
with partitions that are splitting or merging.

I want add CONCURRENTLY mode in future. With this mode partitioned table 
during SPLIT/MERGE operation will be locked with SHARE UPDATE EXCLUSIVE 
(as ATTACH/DETACH PARTITION commands in CONCURRENTLY mode).
But in this case queries from other sessions that want to work with 
partitions that are splitting/merging at this time should receive an 
error (like "Partition data is moving. Repeat the operation later") 
because old partitions will be deleted at the end of SPLIT/MERGE operation.
I hope exists a better solution, but I don't know it now...

-- 
With best regards,
Dmitry Koval

---

From: Zhihong Yu <[email-masked]>
Date: Tue, 31 May 2022 13:43:26 -0700

> P.S. I attached patch with 1-line warning fix (for cfbot).

Hi,
For attachPartTable, the parameter wqueue is missing from comment.
The parameters of CloneRowTriggersToPartition are called parent
and partition. I think it is better to name the parameters to
attachPartTable in a similar manner.

For struct SplitPartContext, SplitPartitionContext would be better name.

contect -> context

Cheers

---

From: Zhihong Yu <[email-masked]>
Date: Tue, 31 May 2022 15:14:25 -0700

> +       /* Store partition contect into list. */
Hi,
For transformPartitionCmdForMerge(), nested loop is used to detect
duplicate names.
If the number of partitions in partcmd->partlist, we should utilize map to
speed up the check.

For check_parent_values_in_new_partitions():

+           if (!find_value_in_new_partitions(&key->partsupfunc[0],
+                                             key->partcollation, parts,
nparts, datum, false))
+               found = false;

It seems we can break out of the loop when found is false.

Cheers

---

From: Dmitry Koval <[email-masked]>
Date: Wed, 1 Jun 2022 21:58:42 +0300

Hi,

1)
> The parameters of CloneRowTriggersToPartition are called parent and partition.
> I think it is better to name the parameters to attachPartTable in a similar manner.
> 

Thanks, changed.

2)
> For transformPartitionCmdForMerge(), nested loop is used to detect duplicate names.
> If the number of partitions in partcmd->partlist, we should utilize map to speed up the check.

I'm not sure what we should utilize map in this case because chance that 
number of merging partitions exceed dozens is low.
Is there a function example that uses a map for such a small number of 
elements?

3)
> It seems we can break out of the loop when found is false.

We have implicit "break" in "for" construction:

I'll change it to explicit "break;" to avoid confusion.

Attached patch with the changes described above.
-- 
With best regards,
Dmitry Koval

index 2de0ebacec3..3a2506d9c79 100644

 
 
 				/*
@@ -17649,6 +17692,37 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
 	}
 }
 
+/*
+ * attachPartTable: 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;
+ * partition: relation of attached partition;
+ * bound: bounds of attached relation.
+ */
+static void
+attachPartTable(List **wqueue, Relation rel, Relation partition, PartitionBoundSpec *bound)
+{
+	/* OK to create inheritance.  Rest of the checks performed there */
+	CreateInheritance(partition, rel);
+
+	/* Update the pg_class entry. */
+	StorePartitionBound(partition, rel, bound);
+
+	/* Ensure there exists a correct set of indexes in the partition. */
+	AttachPartitionEnsureIndexes(rel, partition);
+
+	/* and triggers */
+	CloneRowTriggersToPartition(rel, partition);
+
+	/*
+	 * Clone foreign key constraints.  Callee is responsible for setting up
+	 * for phase 3 constraint verification.
+	 */
+	CloneForeignKeyConstraints(wqueue, rel, partition);
+}
+
 /*
  * ALTER TABLE <name> ATTACH PARTITION <partition-name> FOR VALUES
  *
@@ -17841,23 +17915,8 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
 	check_new_partition_bound(RelationGetRelationName(attachrel), rel,
 							  cmd->bound, pstate);

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -19288,3 +19347,663 @@ GetAttributeCompression(Oid atttypid, char *compression)
 
 	return cmethod;
 }
+
+/*
+typedef struct SplitPartitionContext
+}			SplitPartitionContext;
+ * createSplitPartitionContext: create context for partition and fill it
+static SplitPartitionContext *
+createSplitPartitionContext(Relation partRel)
+	SplitPartitionContext *pc;
+	pc = (SplitPartitionContext *) palloc0(sizeof(SplitPartitionContext));
+ * deleteSplitPartitionContext: delete context for partition
+deleteSplitPartitionContext(SplitPartitionContext * pc, int ti_options)
+	SplitPartitionContext *defaultPartCtx = NULL,
+		pc = createSplitPartitionContext((Relation) lfirst(listptr2));
+		/* Store partition context into list. */
+		pc = createSplitPartitionContext(table_open(defaultPartOid, AccessExclusiveLock));
+	pc = (SplitPartitionContext *) lfirst(list_head(partContexts));
+		SplitPartitionContext *pc;
+			pc = (SplitPartitionContext *) lfirst(listptr);
+		deleteSplitPartitionContext((SplitPartitionContext *) lfirst(listptr), ti_options);
+		deleteSplitPartitionContext(defaultPartCtx, ti_options);
+		if (equal(name, cmd->name))

 	COPY_NODE_FIELD(name);
 	COPY_NODE_FIELD(bound);
 
 	return newnode;
 
+static bool
 
 	return true;

 

+partitions_list:

 
index 1a64a522798..ebf2a8605b0 100644

+/*
+			if (equal(name, name2))

 
 				/*
 
 /*
  * transformPartitionCmd

index 6b35111ebb6..aa332e8207d 100644
@@ -4998,3 +4998,846 @@ satisfies_hash_partition(PG_FUNCTION_ARGS)
 
+	for (i = 0; i < boundinfo->ndatums; i++)
+			if (equal(sps->name, sps2->name))
 
 	return buf->data;
 }
+
+/*
 
+/*

---

From: Zhihong Yu <[email-masked]>
Date: Wed, 1 Jun 2022 12:10:22 -0700

> Attached patch with the changes described above.

Hi,
Thanks for your response.

w.r.t. #2, I think using nested loop is fine for now.
If, when this feature is merged, some user comes up with long merge list,
we can revisit this topic.

Cheers

---

From: Dmitry Koval <[email-masked]>
Date: Wed, 13 Jul 2022 21:27:44 +0300

Hi!

Patch stop applying due to changes in upstream.
Here is a rebased version.

-- 
With best regards,
Dmitry Koval

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f2947ea9b49..0215f224162 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -635,6 +635,11 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);

 /* ----------------------------------------------------------------
  *		DefineRelation
@@ -4387,6 +4392,14 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = ShareUpdateExclusiveLock;
 				break;

 				/*
@@ -4807,6 +4820,16 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
@@ -5212,6 +5235,22 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
@@ -6200,6 +6239,10 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -17623,6 +17666,37 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
 	}
 }
 
+/*
@@ -17815,23 +17889,8 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -19297,3 +19356,663 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
index c018140afe4..ad38e1d456f 100644

 

+partitions_list:

 
@@ -17872,6 +17924,7 @@ unreserved_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PATH
@@ -17939,6 +17992,7 @@ unreserved_keyword:
 			| SIMPLE
 			| SKIP
 			| SNAPSHOT
+			| SPLIT
 			| SQL_P
 			| STABLE
 			| STANDALONE_P
@@ -18490,6 +18544,7 @@ bare_label_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PATH
@@ -18569,6 +18624,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 b57253463b9..8c500014ad3 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"
@@ -135,7 +137,7 @@ static void transformConstraintAttrs(CreateStmtContext *cxt,
 									 List *constraintList);
 static void transformColumnType(CreateStmtContext *cxt, ColumnDef *column);
 static void setSchemaName(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);
@@ -3271,6 +3273,117 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
 }

+/*
+ * Does checks for bounds of new partitions.
+		/* Assign transformed value of the partition bound. */
+	/* Then we should check partitions with transformed bounds. */
@@ -3534,7 +3647,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3542,6 +3655,24 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,

 				/*
@@ -3933,13 +4064,13 @@ setSchemaName(char *context_schema, char **stmt_schema_name)
 
 /*
  * transformPartitionCmd
 
@@ -3948,9 +4079,9 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 
@@ -3958,7 +4089,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 091d6e886b6..c85e602535a 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -4999,3 +4999,846 @@ satisfies_hash_partition(PG_FUNCTION_ARGS)
 
index 26cf4fa9a0e..d9089bc7af6 100644
@@ -12865,3 +12865,21 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+/*
index b0c9c5f2ef6..c351dc48f36 100644
@@ -895,6 +895,17 @@ typedef struct PartitionRangeDatum
 
+/*
@@ -903,6 +914,7 @@ typedef struct PartitionCmd
 
@@ -2302,6 +2314,8 @@ typedef enum AlterTableType
index ae35f032513..58d8ec14b9d 100644

 
diff --git a/src/test/modules/test_oat_hooks/test_oat_hooks.c b/src/test/modules/test_oat_hooks/test_oat_hooks.c
index 900d597f5dd..13b83f39df5 100644
--- a/src/test/modules/test_oat_hooks/test_oat_hooks.c
+++ b/src/test/modules/test_oat_hooks/test_oat_hooks.c
@@ -1644,6 +1644,9 @@ nodetag_to_string(NodeTag tag)
 			return "PartitionRangeDatum";
+			return "SinglePartitionSpec";
 			return "PartitionCmd";
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 34a76ceb60f..4f4f8c9ffff 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2525,6 +2525,7 @@ SimpleStats
 SimpleStringList
 SimpleStringListCell
 SingleBoundSortItem
+SinglePartitionSpec
 Size
 SkipPages
 SlabBlock

---

From: Zhihong Yu <[email-masked]>
Date: Wed, 13 Jul 2022 12:03:46 -0700

Hi,

+attachPartTable(List **wqueue, Relation rel, Relation partition,
PartitionBoundSpec *bound)

I checked naming of existing methods, such as AttachPartitionEnsureIndexes.
I think it would be better if the above method is
named attachPartitionTable.

+   if (!defaultPartCtx && OidIsValid(defaultPartOid))
+   {
+       pc = createSplitPartitionContext(table_open(defaultPartOid,
AccessExclusiveLock));

Since the value of pc would be passed to defaultPartCtx, there is no need
to assign to pc above. You can assign directly to defaultPartCtx.

splitted -> split

need -> needed.

Cheers

---

From: Dmitry Koval <[email-masked]>
Date: Wed, 13 Jul 2022 23:05:44 +0300

Thanks you!
I've fixed all things mentioned.

-- 
With best regards,
Dmitry Koval

index f2947ea9b49..c6a8fffa60b 100644

 
 
 				/*
 
+/*
+ * attachPartitionTable: attach new partition to partitioned table
+ *
+attachPartitionTable(List **wqueue, Relation rel, Relation partition, PartitionBoundSpec *bound)
 
+	attachPartitionTable(wqueue, rel, attachrel, cmd->bound);
 
 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -19297,3 +19356,661 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+		defaultPartCtx = createSplitPartitionContext(table_open(defaultPartOid, AccessExclusiveLock));
+		attachPartitionTable(NULL, rel, newPartRel, sps->bound);
+	/* Drop split partition. */
+	attachPartitionTable(NULL, rel, newPartRel, cmd->bound);
+	/* Rename new partition if it is needed. */

 

+partitions_list:

 

+/*

 
 				/*
 
 /*
  * transformPartitionCmd

index 091d6e886b6..4c55115074b 100644
 
+					 errmsg("partition of hash-partitioned table cannot be split")));
 
 	return buf->data;
 }
+
+/*
 
+/*

 
 
index 00000000000..7caf766f0b4
+--ERROR:  list of split partitions should contains at least two items
 
index 00000000000..6d017f2e919
+--ERROR:  list of split partitions should contains at least two items

---

From: Zhihong Yu <[email-masked]>
Date: Wed, 13 Jul 2022 13:17:30 -0700

> I've fixed all things mentioned.

Hi,
Toward the end of ATExecSplitPartition():

 Why is NoLock passed (instead of AccessExclusiveLock) ?

Cheers

---

From: Dmitry Koval <[email-masked]>
Date: Wed, 13 Jul 2022 23:33:45 +0300

>   Why is NoLock passed (instead of AccessExclusiveLock) ?

Thanks!

You're right, I replaced the comment with "Keep the lock until commit.".

-- 
With best regards,
Dmitry Koval

index f2947ea9b49..0703e36ff76 100644

 
 
 				/*
 
+/*

 	/*
 
 	return cstorage;
 }
+
+/*

 

+partitions_list:

 

+/*

 
 				/*
 
 /*
  * transformPartitionCmd

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

---

From: Alvaro Herrera <[email-masked]>
Date: Thu, 14 Jul 2022 10:12:14 +0200

This is not a review, but I think the isolation tests should be
expanded.  At least, include the case of serializable transactions being
involved.

-- 
Álvaro Herrera        Breisgau, Deutschland  —  https://www.EnterpriseDB.com/
"Pensar que el espectro que vemos es ilusorio no lo despoja de espanto,
sólo le suma el nuevo terror de la locura" (Perelandra, C.S. Lewis)

---

From: Dmitry Koval <[email-masked]>
Date: Fri, 15 Jul 2022 14:00:48 +0300

> expanded.  At least, include the case of serializable transactions being

Thanks!
I will expand the tests for the next commitfest.

-- 
With best regards,
Dmitry Koval

---

From: Dmitry Koval <[email-masked]>
Date: Thu, 11 Aug 2022 09:56:37 +0300

Hi!

-- 
With best regards,
Dmitry Koval

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 70b94bbb39..684df4b80e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -637,6 +637,11 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);

 /* ----------------------------------------------------------------
  *		DefineRelation
@@ -4401,6 +4406,14 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = ShareUpdateExclusiveLock;
 				break;

 				/*
@@ -4822,6 +4835,16 @@ 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 */
+			pass = AT_PASS_MISC;
+			break;
 		default:				/* oops */
 			elog(ERROR, "unrecognized alter table type: %d",
 				 (int) cmd->subtype);
@@ -5243,6 +5266,22 @@ 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);
+			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);
@@ -6231,6 +6270,10 @@ 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:
 			return "ALTER COLUMN ... ADD IDENTITY";
 		case AT_SetIdentity:
@@ -17641,6 +17684,37 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
 	}
 }
 
+/*
@@ -17833,23 +17907,8 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -19314,3 +19373,661 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
index f9037761f9..9162818678 100644
@@ -649,6 +650,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -831,7 +834,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	ORDER ORDINALITY OTHERS OUT_P OUTER_P
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
 
@@ -845,7 +848,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);

@@ -2366,6 +2369,23 @@ alter_table_cmds:
 			| alter_table_cmds ',' alter_table_cmd	{ $$ = lappend($1, $3); }
 		;
 
+partitions_list:
@@ -2376,6 +2396,7 @@ partition_cmd:
 
@@ -2390,6 +2411,7 @@ partition_cmd:
 					n->subtype = AT_DetachPartition;
 					cmd->name = $3;
 					cmd->bound = NULL;
+					cmd->partlist = NULL;
 					cmd->concurrent = $4;
 					n->def = (Node *) cmd;
 
@@ -2403,6 +2425,35 @@ partition_cmd:
@@ -2419,6 +2470,7 @@ index_partition_cmd:
 
@@ -17826,6 +17878,7 @@ unreserved_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PATH
@@ -17893,6 +17946,7 @@ unreserved_keyword:
 			| SIMPLE
 			| SKIP
 			| SNAPSHOT
+			| SPLIT
 			| SQL_P
 			| STABLE
 			| STANDALONE_P
@@ -18444,6 +18498,7 @@ bare_label_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PATH
@@ -18523,6 +18578,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 6d283006e3..5c68756aeb 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"
@@ -135,7 +137,7 @@ static void transformConstraintAttrs(CreateStmtContext *cxt,
 									 List *constraintList);
 static void transformColumnType(CreateStmtContext *cxt, ColumnDef *column);
 static void setSchemaName(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);
@@ -3281,6 +3283,117 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
 }

+/*
@@ -3544,7 +3657,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3552,6 +3665,24 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,

 				/*
@@ -3943,13 +4074,13 @@ setSchemaName(char *context_schema, char **stmt_schema_name)
 
 /*
  * transformPartitionCmd
 
@@ -3958,9 +4089,9 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 
@@ -3968,7 +4099,7 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
index 091d6e886b..4c55115074 100644
 
index d575aa0066..025971f94e 100644
@@ -12901,3 +12901,21 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+/*
index b376031856..3e2fcf302b 100644
 
+/*
 
index ae35f03251..58d8ec14b9 100644
index b1e3f1b840..3630c0eaef 100644
 
index 7d489718a3..ed13a43f53 100644

index 0000000000..34df1345b2
index 529a4cbd4d..22e81b01a4 100644
index 0000000000..e10c7c607a
index 0000000000..c783c4ad09
index 0000000000..7caf766f0b
index 103e11483d..c6f974d7b5 100644
 
index 0000000000..5a724ce65d
index 0000000000..6d017f2e91
index 35c9f1efce..9cd0f11505 100644

---

From: Dmitry Koval <[email-masked]>
Date: Mon, 29 Aug 2022 19:56:47 +0300

Hi!

Combinations of isolation modes (READ COMMITTED/REPEATABLE 
READ/SERIALIZABLE) were added to test

src/test/isolation/specs/partition-split-merge.spec

-- 
With best regards,
Dmitry Koval

index dacc989d855..a4f989b9773 100644

 
 
 				/*
 
+/*

 	/*
 
 	return cstorage;
 }
+
+/*
index b5ab9d9c9a3..7ee85819a8e 100644
@@ -652,6 +653,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -834,7 +837,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	ORDER ORDINALITY OTHERS OUT_P OUTER_P
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
 
@@ -848,7 +851,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);

@@ -2369,6 +2372,23 @@ alter_table_cmds:
 			| alter_table_cmds ',' alter_table_cmd	{ $$ = lappend($1, $3); }
 		;
 
+partitions_list:
@@ -2379,6 +2399,7 @@ partition_cmd:
 
@@ -2393,6 +2414,7 @@ partition_cmd:
 					n->subtype = AT_DetachPartition;
 					cmd->name = $3;
 					cmd->bound = NULL;
+					cmd->partlist = NULL;
 					cmd->concurrent = $4;
 					n->def = (Node *) cmd;
 
@@ -2406,6 +2428,35 @@ partition_cmd:
@@ -2422,6 +2473,7 @@ index_partition_cmd:
 
@@ -17859,6 +17911,7 @@ unreserved_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PATH
@@ -17926,6 +17979,7 @@ unreserved_keyword:
 			| SIMPLE
 			| SKIP
 			| SNAPSHOT
+			| SPLIT
 			| SQL_P
 			| STABLE
 			| STANDALONE_P
@@ -18477,6 +18531,7 @@ bare_label_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PATH
@@ -18556,6 +18611,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 6d283006e3d..5c68756aebc 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"
@@ -135,7 +137,7 @@ static void transformConstraintAttrs(CreateStmtContext *cxt,
 									 List *constraintList);
 static void transformColumnType(CreateStmtContext *cxt, ColumnDef *column);
 static void setSchemaName(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);
@@ -3281,6 +3283,117 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
 }

+/*

 
 				/*
 
 /*
  * transformPartitionCmd

 
index 9959f6910e9..9f3abfdfdd0 100644
@@ -12899,3 +12899,21 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+/*
index 469a5c46f62..efa2de05cfa 100644
 
+/*

 
 
index 00000000000..b2f224cc232
@@ -0,0 +1,388 @@
+step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
+ i|t     
+--+------
+ 5|text05
+ 1|text01
+15|text15
+25|text25
+35|text35
+starting permutation: s1b s1splt s2brr s2i s1c s2c s2s
+step s2brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
+ i|t     
+--+------
+ 5|text05
+ 1|text01
+15|text15
+25|text25
+35|text35
+starting permutation: s1b s1splt s2bs s2i s1c s2c s2s
+step s2bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
+ i|t     
+--+------
+ 5|text05
+ 1|text01
+15|text15
+25|text25
+35|text35
+starting permutation: s1brr s1splt s2b s2i s1c s2c s2s
+step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
+ i|t     
+--+------
+ 5|text05
+ 1|text01
+15|text15
+25|text25
+35|text35
+starting permutation: s1brr s1splt s2brr s2i s1c s2c s2s
+step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
+ i|t     
+--+------
+ 5|text05
+ 1|text01
+15|text15
+25|text25
+35|text35
+starting permutation: s1brr s1splt s2bs s2i s1c s2c s2s
+step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
+ i|t     
+--+------
+ 5|text05
+ 1|text01
+15|text15
+25|text25
+35|text35
+starting permutation: s1bs s1splt s2b s2i s1c s2c s2s
+step s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
+ i|t     
+--+------
+ 5|text05
+ 1|text01
+15|text15
+25|text25
+35|text35
+starting permutation: s1bs s1splt s2brr s2i s1c s2c s2s
+step s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
+ i|t     
+--+------
+ 5|text05
+ 1|text01
+15|text15
+25|text25
+35|text35
+starting permutation: s1bs s1splt s2bs s2i s1c s2c s2s
+step s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
+ i|t     
+--+------
+ 5|text05
+ 1|text01
+15|text15
+25|text25
+35|text35
+step s2i: INSERT INTO tpart VALUES (1, 'text01');
+step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; <waiting ...>
+ i|t          
+--+-----------
+ 5|text05     
+15|text15     
+ 1|text01modif
+25|text25     
+35|text35     
+starting permutation: s2b s2i s2c s1brr s1merg s2b s2u s1c s2c s2s
+step s2i: INSERT INTO tpart VALUES (1, 'text01');
+step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; <waiting ...>
+ i|t          
+--+-----------
+ 5|text05     
+15|text15     
+ 1|text01modif
+25|text25     
+35|text35     
+starting permutation: s2b s2i s2c s1bs s1merg s2b s2u s1c s2c s2s
+step s2i: INSERT INTO tpart VALUES (1, 'text01');
+step s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; <waiting ...>
+ i|t          
+--+-----------
+ 5|text05     
+15|text15     
+ 1|text01modif
+25|text25     
+35|text35     
+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 s2u: UPDATE tpart SET t = 'text01modif' where i = 1; <waiting ...>
+ i|t          
+--+-----------
+ 5|text05     
+15|text15     
+ 1|text01modif
+25|text25     
+35|text35     
+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 s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; <waiting ...>
+ i|t          
+--+-----------
+ 5|text05     
+15|text15     
+ 1|text01modif
+25|text25     
+35|text35     
+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 s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; <waiting ...>
+ i|t          
+--+-----------
+ 5|text05     
+15|text15     
+ 1|text01modif
+25|text25     
+35|text35     
+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 s2u: UPDATE tpart SET t = 'text01modif' where i = 1; <waiting ...>
+ i|t          
+--+-----------
+ 5|text05     
+15|text15     
+ 1|text01modif
+25|text25     
+35|text35     
+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 s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; <waiting ...>
+ i|t          
+--+-----------
+ 5|text05     
+15|text15     
+ 1|text01modif
+25|text25     
+35|text35     
+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 s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; <waiting ...>
+ i|t          
+--+-----------
+ 5|text05     
+15|text15     
+ 1|text01modif
+25|text25     
+35|text35     
index 00000000000..1da8f642d10
@@ -0,0 +1,73 @@
+  INSERT INTO tpart VALUES (5, 'text05');
+  INSERT INTO tpart VALUES (15, 'text15');
+  INSERT INTO tpart VALUES (25, 'text25');
+  INSERT INTO tpart VALUES (35, 'text35');
+step s1brr	{ BEGIN ISOLATION LEVEL REPEATABLE READ; }
+step s1bs	{ BEGIN ISOLATION LEVEL SERIALIZABLE; }
+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; }
+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
+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
 
index 8ad112c44d0..bd1e6f0c01a 100644
@@ -2528,6 +2528,7 @@ SimpleStats
 SimpleStringList
 SimpleStringListCell
 SingleBoundSortItem
+SinglePartitionSpec
 Size
 SkipPages
 SlabBlock

---

From: Dmitry Koval <[email-masked]>
Date: Wed, 7 Sep 2022 20:03:09 +0300

Hi!

-- 
With best regards,
Dmitry Koval

index dacc989d85..a4f989b977 100644

 
 
 				/*
 
+/*

 	/*
 
 	return cstorage;
 }
+
+/*
index 0492ff9a66..c829872295 100644
@@ -645,6 +646,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -731,7 +734,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
@@ -745,7 +748,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 	SAVEPOINT 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
 
@@ -2254,6 +2257,23 @@ alter_table_cmds:
 			| alter_table_cmds ',' alter_table_cmd	{ $$ = lappend($1, $3); }
 		;
 
+partitions_list:
@@ -2264,6 +2284,7 @@ partition_cmd:
 
@@ -2278,6 +2299,7 @@ partition_cmd:
 					n->subtype = AT_DetachPartition;
 					cmd->name = $3;
 					cmd->bound = NULL;
+					cmd->partlist = NULL;
 					cmd->concurrent = $4;
 					n->def = (Node *) cmd;
 
@@ -2291,6 +2313,35 @@ partition_cmd:
@@ -2307,6 +2358,7 @@ index_partition_cmd:
 
@@ -16829,6 +16881,7 @@ unreserved_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PLANS
@@ -16892,6 +16945,7 @@ unreserved_keyword:
 			| SIMPLE
 			| SKIP
 			| SNAPSHOT
+			| SPLIT
 			| SQL_P
 			| STABLE
 			| STANDALONE_P
@@ -17410,6 +17464,7 @@ bare_label_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PLACING
@@ -17485,6 +17540,7 @@ bare_label_keyword:
 			| SMALLINT
 			| SNAPSHOT
 			| SOME
+			| SPLIT
 			| SQL_P
 			| STABLE
 			| STANDALONE_P

+/*

 
 				/*
 
 /*
  * transformPartitionCmd

 
index 2b7b1b0c0f..7ec7413c1b 100644
@@ -12228,3 +12228,21 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+/*
index 6958306a7d..2e44f2db77 100644
 
+/*
 
@@ -2015,6 +2027,8 @@ typedef enum AlterTableType
index 9a7cc0c6bd..a3569cad0b 100644
@@ -313,6 +313,7 @@ PG_KEYWORD("parameter", PARAMETER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
@@ -391,6 +392,7 @@ PG_KEYWORD("skip", SKIP, UNRESERVED_KEYWORD, BARE_LABEL)

 
index 0000000000..b2f224cc23
index 0000000000..1da8f642d1
index 9f644a0c1b..b975fe295c 100644
 
index 5b3b305963..1626acd430 100644
@@ -2514,6 +2514,7 @@ SimpleStats
 SimpleStringList
 SimpleStringListCell
 SingleBoundSortItem
+SinglePartitionSpec
 Size
 SkipPages
 SlabBlock

---

From: Justin Pryzby <[email-masked]>
Date: Wed, 7 Sep 2022 13:43:34 -0500

> Here is a rebased version.

This crashes on freebsd with -DRELCACHE_FORCE_RELEASE
https://cirrus-ci.com/task/6565371623768064
https://cirrus-ci.com/task/6145355992530944

Note that that's a modified cirrus script from my CI improvements branch
which also does some extra/different things.

-- 
Justin

---

From: Dmitry Koval <[email-masked]>
Date: Thu, 8 Sep 2022 14:35:24 +0300

Thanks a lot Justin!

After compilation PostgreSQL+patch with macros
RELCACHE_FORCE_RELEASE,
COPY_PARSE_PLAN_TREES,
WRITE_READ_PARSE_PLAN_TREES,
RAW_EXPRESSION_COVERAGE_TEST,
RANDOMIZE_ALLOCATED_MEMORY,
I saw a problem on Windows 10, MSVC2019.

(I hope this problem was the same as on Cirrus CI).

Attached patch with fix.

-- 
With best regards,
Dmitry Koval

index dacc989d85..3c8c7aec9e 100644

 
 
 				/*
 
+/*

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -19314,3 +19373,665 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+		Relation defaultPartRel = defaultPartCtx->partRel;
+		table_close(defaultPartRel, NoLock);
+		/* Get relation id before table_close() call. */

 

+partitions_list:

 

+/*

 
 				/*
 
 /*
  * transformPartitionCmd

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

---

From: Justin Pryzby <[email-masked]>
Date: Thu, 8 Sep 2022 07:26:04 -0500

> I saw a problem on Windows 10, MSVC2019.

Yes, it passes tests on my CI improvements branch.
https://github.com/justinpryzby/postgres/runs/8248668269
Thanks to Alexander Pyhalov for reminding me about
RELCACHE_FORCE_RELEASE last year ;)

On Tue, May 31, 2022 at 12:32:43PM +0300, Dmitry Koval wrote:
> This can be useful for this example cases: 
> need to merge all one-day partitions
> into a month partition.

+1, we would use this (at least the MERGE half).

I wonder if it's possible to reduce the size of this patch (I'm starting
to try to absorb it).  Is there a way to refactor/reuse existing code to
reduce its footprint ?

partbounds.c is adding 500+ LOC about checking if proposed partitions
meet the requirements (don't overlap, etc).  But a lot of those checks
must already happen, no?  Can you re-use/refactor the existing checks ?

An UPDATE on a partitioned table will move tuples from one partition to
another.  Is there a way to re-use that ?  Also, postgres already
supports concurrent DDL (CREATE+ATTACH and DETACH CONCURRENTLY).  Is it 
possible to leverage that ?  (Mostly to reduce the patch size, but also
because maybe some cases could be concurrent?).

If the patch were split into separate parts for MERGE and SPLIT, would
the first patch be significantly smaller than the existing patch
(hopefully half as big) ?  That would help to review it, even if both
halves were ultimately squished together.  (An easy way to do this is to
open up all the files in separate editor instances, trim out the parts
that aren't needed for the first patch, save the files but don't quit
the editors, test compilation and regression tests, then git commit
--amend -a.  Then in each editor, "undo" all the trimmed changes, save,
and git commit -a).

Would it save much code if "default" partitions weren't handled in the
first patch ?

-- 
Justin

---

From: Alvaro Herrera <[email-masked]>
Date: Thu, 8 Sep 2022 16:10:54 +0200

> --amend -a.  Then in each editor, "undo" all the trimmed changes, save,

An easier (IMO) way to do that is to use "git gui" or even "git add -p",
which allow you to selectively add changed lines/hunks to the index.
You add a few, commit, then add the rest, commit again.  With "git add
-p" you can even edit individual hunks in an editor in case you have a
mix of both wanted and unwanted in a single hunk (after "s"plitting, of
course), which turns out to be easier than it sounds.

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"El sudor es la mejor cura para un pensamiento enfermo" (Bardia)

---

From: Dmitry Koval <[email-masked]>
Date: Thu, 8 Sep 2022 17:26:51 +0300

Thanks for your advice, Justin and Alvaro!

I'll try to reduce the size of this patch and split it into separate 
parts (for MERGE and SPLIT).

-- 
With best regards,
Dmitry Koval

---

From: Dmitry Koval <[email-masked]>
Date: Mon, 19 Sep 2022 22:26:28 +0300

Hi!

Two separate parts for MERGE and SPLIT partitions (without refactoring; 
it will be later)

-- 
With best regards,
Dmitry Koval

From 20317e7f2dbd65cbbaa075092db50ac97f6dd03c Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email-masked]>
Date: Mon, 19 Sep 2022 16:06:03 +0300
Subject: [PATCH v11 1/2] ALTER TABLE MERGE PARTITIONS command

---
 src/backend/commands/tablecmds.c              | 347 ++++++++-
 src/backend/parser/gram.y                     |  22 +-
 src/backend/parser/parse_utilcmd.c            |  89 +++
 src/backend/partitioning/partbounds.c         | 184 +++++
 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 ++
 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 +
 14 files changed, 2070 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 e3233a8f38..9ae3855673 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -637,6 +637,8 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);

 /* ----------------------------------------------------------------
  *		DefineRelation
@@ -4401,6 +4403,10 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = ShareUpdateExclusiveLock;
 				break;

 				/*
@@ -4822,6 +4828,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);
@@ -5243,6 +5254,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);
@@ -6231,6 +6250,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:
@@ -17653,6 +17674,37 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
 	}
 }
 
+/*
@@ -17845,23 +17897,8 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -19326,3 +19363,279 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
index 82f03fc9c9..c48a732f5d 100644
@@ -728,7 +728,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -2261,6 +2261,7 @@ partition_cmd:
 
@@ -2275,6 +2276,7 @@ partition_cmd:
 					n->subtype = AT_DetachPartition;
 					cmd->name = $3;
 					cmd->bound = NULL;
+					cmd->partlist = NULL;
 					cmd->concurrent = $4;
 					n->def = (Node *) cmd;
 
@@ -2288,6 +2290,21 @@ partition_cmd:
@@ -2304,6 +2321,7 @@ index_partition_cmd:
 
@@ -16826,6 +16844,7 @@ unreserved_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PLANS
@@ -17407,6 +17426,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 6d283006e3..8dfb94acb4 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"
@@ -3281,6 +3283,80 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
 }

+/*
@@ -3552,6 +3628,19 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,

 				/*
diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index 57c9b51814..2068565ed2 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -5000,3 +5000,187 @@ satisfies_hash_partition(PG_FUNCTION_ARGS)
 
index 6958306a7d..c609f92deb 100644
 
+/*
 
@@ -2015,6 +2027,7 @@ typedef enum AlterTableType
index 9a7cc0c6bd..d0d76260bc 100644
index b1e3f1b840..850b1add50 100644
@@ -143,4 +143,10 @@ extern int	partition_range_datum_bsearch(FmgrInfo *partsupfunc,
 
diff --git a/src/test/isolation/expected/partition-merge.out b/src/test/isolation/expected/partition-merge.out
index 0000000000..98446aaab5
+++ b/src/test/isolation/expected/partition-merge.out
@@ -0,0 +1,199 @@
index 5413a59a80..d91032c42a 100644
@@ -102,6 +102,7 @@ test: partition-key-update-1
+test: partition-merge
diff --git a/src/test/isolation/specs/partition-merge.spec b/src/test/isolation/specs/partition-merge.spec
index 0000000000..ec48732c58
+++ b/src/test/isolation/specs/partition-merge.spec
@@ -0,0 +1,54 @@
+# Verify that MERGE operation locks DML operations with partitioned table
index 0000000000..013ab65961
@@ -0,0 +1,736 @@
+-- Merge some partitions into DEFAULT partition
index 9f644a0c1b..5153c923bf 100644
+test: partition_merge partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats
 
index 0000000000..8bf90fd621
@@ -0,0 +1,434 @@
+-- Merge some partitions into DEFAULT partition
2.31.0.windows.1

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

---
 src/backend/commands/tablecmds.c              |  408 +++++
 src/backend/parser/gram.y                     |   38 +-
 src/backend/parser/parse_utilcmd.c            |   62 +-
 src/backend/partitioning/partbounds.c         |  659 ++++++++
 src/backend/utils/adt/ruleutils.c             |   18 +
 src/include/nodes/parsenodes.h                |    1 +
 src/include/parser/kwlist.h                   |    1 +
 src/include/partitioning/partbounds.h         |    4 +
 src/include/utils/ruleutils.h                 |    2 +
 .../isolation/expected/partition-split.out    |  190 +++
 src/test/isolation/isolation_schedule         |    1 +
 src/test/isolation/specs/partition-split.spec |   54 +
 src/test/regress/expected/partition_split.out | 1425 +++++++++++++++++
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/partition_split.sql      |  839 ++++++++++
 15 files changed, 3692 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 9ae3855673..b2778aefe5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -637,6 +637,9 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, 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);
 
@@ -4403,6 +4406,10 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = ShareUpdateExclusiveLock;
 				break;
 
+			case AT_SplitPartition:
+				cmd_lockmode = AccessExclusiveLock;
+				break;
+
 			case AT_MergePartitions:
 				cmd_lockmode = AccessExclusiveLock;
 				break;
@@ -4828,6 +4835,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 */
@@ -5254,6 +5266,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);
@@ -6250,6 +6270,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:
@@ -19364,6 +19386,256 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 	return cstorage;
 }
 
+/*
  * createPartitionTable: create table for new partition with given name
  * (newPartName) like table (modelRelName)
@@ -19418,6 +19690,142 @@ createPartitionTable(RangeVar *newPartName, RangeVar *modelRelName,
 				   NULL);
 }
 
+/*
  * Struct with context of merged partition
index c48a732f5d..083071e4c4 100644
@@ -270,6 +270,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -642,6 +643,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -742,7 +745,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);

@@ -2251,6 +2254,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
@@ -2295,6 +2315,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
 				{
@@ -16908,6 +16942,7 @@ unreserved_keyword:
 			| SIMPLE
 			| SKIP
 			| SNAPSHOT
+			| SPLIT
 			| SQL_P
 			| STABLE
 			| STANDALONE_P
@@ -17502,6 +17537,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 8dfb94acb4..5c68756aeb 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -137,7 +137,7 @@ static void transformConstraintAttrs(CreateStmtContext *cxt,
 									 List *constraintList);
 static void transformColumnType(CreateStmtContext *cxt, ColumnDef *column);
 static void setSchemaName(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);
@@ -3283,6 +3283,43 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
 }

+/*
  * transformPartitionCmdForMerge
  *		Analyze the ALTER TABLLE ... MERGE PARTITIONS command
@@ -3620,7 +3657,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3628,6 +3665,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
+			case AT_SplitPartition:
 			case AT_MergePartitions:
 				{
 					PartitionCmd *partcmd = (PartitionCmd *) cmd->def;
@@ -3636,7 +3674,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;
 				}
@@ -4032,13 +4074,13 @@ setSchemaName(char *context_schema, char **stmt_schema_name)
 
 /*
  * transformPartitionCmd
 
@@ -4047,9 +4089,9 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 
@@ -4057,7 +4099,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 2068565ed2..5283870e32 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -5046,6 +5046,665 @@ check_partitions_not_overlap_range(Relation parent,
 	}
 }
 
+/*
  * get_partition_bound_spec
 
 	return buf->data;
 }
+
+/*
index c609f92deb..2e44f2db77 100644
@@ -2027,6 +2027,7 @@ typedef enum AlterTableType
 	AT_MergePartitions,			/* MERGE PARTITIONS */
index d0d76260bc..a3569cad0b 100644
@@ -392,6 +392,7 @@ PG_KEYWORD("skip", SKIP, UNRESERVED_KEYWORD, BARE_LABEL)
index 850b1add50..3630c0eaef 100644
@@ -143,6 +143,10 @@ extern int	partition_range_datum_bsearch(FmgrInfo *partsupfunc,
 
 extern void calculate_partition_bound_for_merge(Relation parent,
 												List *partNames,
 												List *partOids,

diff --git a/src/test/isolation/expected/partition-split.out b/src/test/isolation/expected/partition-split.out
index 0000000000..5d9e8b0925
+++ b/src/test/isolation/expected/partition-split.out
@@ -0,0 +1,190 @@
index d91032c42a..cbaf030858 100644
@@ -103,6 +103,7 @@ test: partition-key-update-2
 test: partition-merge
+test: partition-split
diff --git a/src/test/isolation/specs/partition-split.spec b/src/test/isolation/specs/partition-split.spec
index 0000000000..087239a4a1
+++ b/src/test/isolation/specs/partition-split.spec
@@ -0,0 +1,54 @@
+# Verify that SPLIT operation locks DML operations with partitioned table
index 5153c923bf..b975fe295c 100644
-test: partition_merge partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats
 
2.31.0.windows.1

---

From: Robert Haas <[email-masked]>
Date: Mon, 19 Sep 2022 15:56:42 -0400

> For hash partitioning (BY HASH) these operations are not supported.

This may be a good idea, but I would like to point out one
disadvantage of this approach.

If you know that a certain partition is not changing, and you would
like to split it, you can create two or more new standalone tables and
populate them from the original partition using INSERT .. SELECT. Then
you can BEGIN a transaction, DETACH the existing partitions, and
ATTACH the replacement ones. By doing this, you take an ACCESS
EXCLUSIVE lock on the partitioned table only for a brief period. The
same kind of idea can be used to merge partitions.

It seems hard to do something comparable with built-in DDL for SPLIT
PARTITION and MERGE PARTITION. You could start by taking e.g. SHARE
lock on the existing partition(s) and then wait until the end to take
ACCESS EXCLUSIVE lock on the partitions, but we typically avoid such
coding patterns, because the lock upgrade might deadlock and then a
lot of work would be wasted. So most likely with the approach you
propose here you will end up acquiring ACCESS EXCLUSIVE lock at the
beginning of the operation and then shuffle a lot of data around while
still holding it, which is pretty painful.

Because of this problem, I find it hard to believe that these commands
would get much use, except perhaps on small tables or in
non-production environments, unless people just didn't know about the
alternatives. That's not to say that something like this has no value.
As a convenience feature, it's fine. It's just hard for me to see it
as any more than that.

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

---

From: Dmitry Koval <[email-masked]>
Date: Mon, 19 Sep 2022 23:42:38 +0300

Thanks for comments and advice!
I thought about this problem and discussed about it with colleagues.
Unfortunately, I don't know of a good general solution.

> same kind of idea can be used to merge partitions.

But for specific situation like this (certain partition is not changing) 
we can add CONCURRENTLY modifier.
Our DDL query can be like

ALTER TABLE...SPLIT PARTITION [CONCURRENTLY];

With CONCURRENTLY modifier we can lock partitioned table in 
ShareUpdateExclusiveLock mode and split partition - in 
AccessExclusiveLock mode. So we don't lock partitioned table in 
AccessExclusiveLock mode and can modify other partitions during SPLIT 
operation (except split partition).
If smb try to modify split partition, he will receive error "relation 
does not exist" at end of operation (because split partition will be drop).

-- 
With best regards,
Dmitry Koval

---

From: Robert Haas <[email-masked]>
Date: Tue, 20 Sep 2022 08:20:52 -0400

> Unfortunately, I don't know of a good general solution.

Yeah, me neither.

> does not exist" at end of operation (because split partition will be drop).

I think that a built-in DDL command can't really assume that the user
won't modify anything. You'd have to take a ShareLock.

But you might be able to have a CONCURRENTLY variant of the command
that does the same kind of multi-transaction thing as, e.g., CREATE
INDEX CONCURRENTLY. You would probably have to be quite careful about
race conditions (e.g. you commit the first transaction and before you
start the second one, someone drops or detaches the partition you were
planning to merge or split). Might take some thought, but feels
possibly doable. I've never been excited enough about this kind of
thing to want to put a lot of energy into engineering it, because
doing it "manually" feels so much nicer to me, and doubly so given
that we now have ATTACH CONCURRENTLY and DETACH CONCURRENTLY, but it
does seem like a thing some people would probably use and value.

---

From: Dmitry Koval <[email-masked]>
Date: Tue, 11 Oct 2022 19:21:54 +0300

Hi!

Fixed couple warnings (for cfbot).

-- 
With best regards,
Dmitry Koval

From 357187cefcef4e9340f5c6798ddeef9e4e62f15d Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email-masked]>
Date: Mon, 19 Sep 2022 16:06:03 +0300
Subject: [PATCH v12 1/2] ALTER TABLE MERGE PARTITIONS command

---

index 20135ef1b0..c6e1ba6dca 100644

 
 
 				/*
@@ -17639,6 +17660,37 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
 	}
 }
 
+/*
@@ -17831,23 +17883,8 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -19312,3 +19349,279 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
index 94d5142a4a..81443151e8 100644
@@ -727,7 +727,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);

 
 
@@ -16833,6 +16851,7 @@ unreserved_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PLANS
@@ -17415,6 +17434,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 bd068bba05..02521e1b6d 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"
@@ -3281,6 +3283,80 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
 }

+/*

 				/*
index 0823fa7b1d..1fa0a73978 100644
 
index 633e7671b3..6c294df786 100644
@@ -896,6 +896,17 @@ typedef struct PartitionRangeDatum
 
+/*
@@ -904,6 +915,7 @@ typedef struct PartitionCmd
 
@@ -2016,6 +2028,7 @@ typedef enum AlterTableType
index ccc927851c..09b028acba 100644
index 1f5b706d83..435668fb7c 100644

index d9b839c979..fffb780b7c 100644
@@ -2515,6 +2515,7 @@ SimpleStats
 SimpleStringList
 SimpleStringListCell
 SingleBoundSortItem
+SinglePartitionSpec
 Size
 SkipPages
 SlabBlock
-- 
2.31.0.windows.1

From e5b71c79bb1c308e32bb5024474413cbc44a24e7 Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email-masked]>
Date: Mon, 19 Sep 2022 17:58:52 +0300
Subject: [PATCH v12 2/2] ALTER TABLE SPLIT PARTITION command

---
 src/backend/commands/tablecmds.c              |  407 +++++
 15 files changed, 3691 insertions(+), 12 deletions(-)

index c6e1ba6dca..c230c0f894 100644

 
@@ -19350,6 +19372,255 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 	return cstorage;
 }
 
+/*
@@ -19404,6 +19675,142 @@ createPartitionTable(RangeVar *newPartName, RangeVar *modelRelName,
 				   NULL);
 }
 
+/*
index 81443151e8..bdadeb6066 100644
@@ -269,6 +269,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -641,6 +642,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -741,7 +744,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 	SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER

+partitions_list:
@@ -16915,6 +16949,7 @@ unreserved_keyword:
@@ -17510,6 +17545,7 @@ bare_label_keyword:
index 02521e1b6d..ada9d6c754 100644

+/*

 
 /*
  * transformPartitionCmd

index 1fa0a73978..4690a1d8f1 100644
 
+/*
+					datum = list_nth(first ?
index 855d48372a..0f39312efd 100644
@@ -12232,3 +12232,21 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+/*
index 6c294df786..0ab6daea0b 100644
@@ -2028,6 +2028,7 @@ typedef enum AlterTableType
index 09b028acba..192341c0c3 100644
index 435668fb7c..d496d6ac4b 100644

---

From: Zhihong Yu <[email-masked]>
Date: Tue, 11 Oct 2022 09:58:01 -0700

> Fixed couple warnings (for cfbot).

Hi,
For v12-0001-PGPRO-ALTER-TABLE-MERGE-PARTITIONS-command.patch:

+       if (equal(name, cmd->name))
+           /* One new partition can have the same name as merged
partition. */
+           isSameName = true;

I think there should be a check before assigning true to isSameName - if
isSameName is true, that means there are two partitions with this same name.

Cheers

---

From: Zhihong Yu <[email-masked]>
Date: Tue, 11 Oct 2022 10:15:05 -0700

> isSameName is true, that means there are two partitions with this same name.

Pardon - I see that transformPartitionCmdForMerge() compares the partition
names.
Maybe you can add a comment in ATExecMergePartitions referring to
transformPartitionCmdForMerge() so that people can more easily understand
the logic.

---

From: Dmitry Koval <[email-masked]>
Date: Thu, 13 Oct 2022 11:57:33 +0300

Hi!

 >transformPartitionCmdForMerge() so that people can more easily
 >understand the logic.

Thanks, comment added.

Here is a fixed version.

-- 
With best regards,
Dmitry Koval

From e191d4c4959cb999c679309342a3425658b074f8 Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email-masked]>
Date: Mon, 19 Sep 2022 16:06:03 +0300
Subject: [PATCH v13 1/2] ALTER TABLE MERGE PARTITIONS command

---
 src/backend/commands/tablecmds.c              | 351 ++++++++-
 14 files changed, 2074 insertions(+), 19 deletions(-)

index 20135ef1b0..2deae2b0fb 100644

 
 
 				/*
 
+/*

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -19312,3 +19349,283 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+		 * Checking that two partitions have the same name was before,
+		 * in function transformPartitionCmdForMerge().

 

+/*

 				/*

+/*

 

From 87299755634340446123b87ff1205109643267b7 Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email-masked]>
Date: Mon, 19 Sep 2022 17:58:52 +0300
Subject: [PATCH v13 2/2] ALTER TABLE SPLIT PARTITION command

---

index 2deae2b0fb..ebcb651a22 100644

 
 
+/*
 
+/*

 
 
+partitions_list:

+/*

 
 /*
  * transformPartitionCmd

index 1fa0a73978..24418fa8ba 100644
 
+/*
+			PartitionRangeDatum *datum = list_nth(spec->lowerdatums, abs(cmpval) - 1);
+				list_nth(spec->lowerdatums, abs(cmpval) - 1);
+									 abs(cmpval) - 1);
 
 	return buf->data;
 }
+
+/*

---

From: Dmitry Koval <[email-masked]>
Date: Tue, 29 Nov 2022 01:30:14 +0300

I'm sorry, I couldn't answer earlier...

1.
 > meet the requirements (don't overlap, etc).  But a lot of those
 > checks must already happen, no?  Can you re-use/refactor the existing
 > checks ?

I a bit reduced the number of lines in partbounds.c and added comments.
Unfortunately, it is very difficult to re-use existing checks for other 
partitioned tables operations, because mostly part of PostgreSQL 
commands works with a single partition.
So for SPLIT/MERGE commands were created new checks for several partitions.

2.
 > Also, postgres already supports concurrent DDL (CREATE+ATTACH and
 > DETACH CONCURRENTLY).  Is it possible to leverage that ?
 > (Mostly to reduce the patch size, but also because maybe some cases
 > could be concurrent?).

Probably "ATTACH CONCURRENTLY" is not supported?
A few words about "DETACH CONCURRENTLY".
"DETACH CONCURRENTLY" can works because this command not move rows 
during detach partition (and so no reason to block detached partition).
"DETACH CONCURRENTLY" do not changes data, but changes partition 
description (partition is marked as "inhdetachpending = true" etc.).

For SPLIT and MERGE the situation is completely different - these 
commands transfer rows between sections.
Therefore partitions must be LOCKED EXCLUSIVELY during rows transfer.
Probably we can use concurrently partitions not participating in SPLIT 
and MERGE.
But now PostgreSQL has no possibilities to forbid using a part of 
partitions of a partitioned table (until the end of data transfer by 
SPLIT/MERGE commands).
Simple locking is not quite suitable here.
I see only one variant of SPLIT/MERGE CONCURRENTLY implementation that 
can be realized now:

* ShareUpdateExclusiveLock on partitioned table;
* AccessExclusiveLock on partition(s) which will be deleted and will be 
created during SPLIT/MEGRE command;
* transferring data between locked sections; operations with non-blocked 
partitions are allowed;
* sessions which want to use partition(s) which will be deleted, waits 
on locks;
* finally we release AccessExclusiveLock on partition(s) which will be 
deleted and delete them;
* waiting sessions will get errors "relation ... does not exist" (we can 
transform it to "relation structure was changed ... please try again"?).

It doesn't look pretty.
Therefore for the SPLIT/MERGE command the partitioned table is locked 
with AccessExclusiveLock.

3.
 > An UPDATE on a partitioned table will move tuples from one partition
 > to another.  Is there a way to re-use that?

This could be realized using methods that are called from 
ExecCrossPartitionUpdate().
But using these methods is more expensive than the current 
implementation of the SPLIT/MERGE commands.
SPLIT/MERGE commands uses "bulk insert" and there is low overhead for 
finding a partition to insert data: for MERGE is not need to search 
partition; for SPLIT need to use simple search from several partitions 
(listed in the SPLIT command).
Below is a test example.

a. Transferring data from the table "test2" to partitions "partition1" 
and "partition2" using the current implementation of tuple routing in 
PostgreSQL:

CREATE TABLE test (a int, b char(10)) PARTITION BY RANGE (a);
CREATE TABLE partition1 PARTITION OF test FOR VALUES FROM (10) TO (20);
CREATE TABLE partition2 PARTITION OF test FOR VALUES FROM (20) TO (30);
CREATE TABLE test2 (a int, b char(10));
INSERT INTO test2 (a, b) SELECT 11, 'a' FROM generate_series(1, 1000000);
INSERT INTO test2 (a, b) SELECT 22, 'b' FROM generate_series(1, 1000000);
INSERT INTO test(a, b) SELECT a, b FROM test2;
DROP TABLE test2;
DROP TABLE test;

Three attempts (the results are little different), the best result:

INSERT 0 2000000
Time: 4467,814 ms (00:04,468)

b. Transferring data from the partition "partition0" to partitions 
"partition 1" and "partition2" using SPLIT command:

CREATE TABLE test (a int, b char(10)) PARTITION BY RANGE (a);
CREATE TABLE partition0 PARTITION OF test FOR VALUES FROM (0) TO (30);
INSERT INTO test (a, b) SELECT 11, 'a' FROM generate_series(1, 1000000);
INSERT INTO test (a, b) SELECT 22, 'b' FROM generate_series(1, 1000000);
ALTER TABLE test SPLIT PARTITION partition0 INTO
   (PARTITION partition0 FOR VALUES FROM (0) TO (10),
    PARTITION partition1 FOR VALUES FROM (10) TO (20),
    PARTITION partition2 FOR VALUES FROM (20) TO (30));
DROP TABLE test;

Three attempts (the results are little different), the best result:

ALTER TABLE
Time: 3840,127 ms (00:03,840)

So the current implementation of tuple routing is ~16% slower than the 
SPLIT command.
That's quite a lot.

With best regards,
Dmitry Koval

Postgres Professional: http://postgrespro.com
From cfb275aa70ddb94037d72416bec8e6693f6355e7 Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email-masked]>
Date: Mon, 19 Sep 2022 16:06:03 +0300
Subject: [PATCH v14 1/2] ALTER TABLE MERGE PARTITIONS command

---
 src/backend/partitioning/partbounds.c         | 207 +++++
 14 files changed, 2097 insertions(+), 19 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 845208d662..3e16019f8a 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -638,6 +638,8 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);

 
 
 				/*
@@ -17645,6 +17666,37 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
 	}
 }
 
+/*
@@ -17837,23 +17889,8 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,

 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -19331,3 +19368,283 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
index 9384214942..239d3831df 100644
@@ -726,7 +726,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -2260,6 +2260,7 @@ partition_cmd:
 
@@ -2274,6 +2275,7 @@ partition_cmd:
 					n->subtype = AT_DetachPartition;
 					cmd->name = $3;
 					cmd->bound = NULL;
+					cmd->partlist = NULL;
 					cmd->concurrent = $4;
 					n->def = (Node *) cmd;
 
@@ -2287,6 +2289,21 @@ partition_cmd:
@@ -2303,6 +2320,7 @@ index_partition_cmd:
 
@@ -16878,6 +16896,7 @@ unreserved_keyword:
 			| PARSER
 			| PARTIAL
 			| PARTITION
+			| PARTITIONS
 			| PASSING
 			| PASSWORD
 			| PLANS
@@ -17460,6 +17479,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 487eb2041b..a14ba6d2d0 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"
@@ -3281,6 +3283,80 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
 }

+/*

 				/*
diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index 29643fb4ab..b5eaf0b7ad 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -4983,3 +4983,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 f4ed9bbff9..f9487417de 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -897,6 +897,17 @@ typedef struct PartitionRangeDatum
 	int			location;		/* token location, or -1 if unknown */
 } PartitionRangeDatum;
 
+/*
@@ -905,6 +916,7 @@ typedef struct PartitionCmd
 
@@ -2017,6 +2029,7 @@ typedef enum AlterTableType
index 957ee18d84..1ab92a2afe 100644
index a32fc6c149..57804298df 100644
 
index 9a139f1e24..d1924bb337 100644
 
index 2f5802195d..4b010a7d81 100644
@@ -2513,6 +2513,7 @@ SimpleStats
 SimpleStringList
 SimpleStringListCell
 SingleBoundSortItem
+SinglePartitionSpec
 Size
 SkipPages
 SlabBlock
-- 
2.31.0.windows.1

From 17139ca416684b863f6f057cbe856f5e29b8cf99 Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email-masked]>
Date: Mon, 19 Sep 2022 17:58:52 +0300
Subject: [PATCH v14 2/2] 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 +
 src/test/regress/expected/partition_split.out | 1417 +++++++++++++++++
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/partition_split.sql      |  833 ++++++++++
 15 files changed, 3674 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 3e16019f8a..cdc54046f6 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -638,6 +638,9 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);

 
@@ -19369,6 +19391,255 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 	return cstorage;
 }
 
+/*
@@ -19423,6 +19694,142 @@ createPartitionTable(RangeVar *newPartName, RangeVar *modelRelName,
 				   NULL);
 }
 
+/*
index 239d3831df..fa4887c48a 100644
@@ -268,6 +268,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -640,6 +641,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -740,7 +743,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);

@@ -2250,6 +2253,23 @@ alter_table_cmds:
 			| alter_table_cmds ',' alter_table_cmd	{ $$ = lappend($1, $3); }
 		;
 
+partitions_list:
@@ -2294,6 +2314,20 @@ partition_cmd:
@@ -16960,6 +16994,7 @@ unreserved_keyword:
@@ -17555,6 +17590,7 @@ bare_label_keyword:
index a14ba6d2d0..0dfac06f84 100644

+/*
+	check_partitions_for_split(parent, splitPartOid, partcmd->name, partcmd->partlist, cxt->pstate);

 
 /*
  * transformPartitionCmd

index b5eaf0b7ad..99659604a5 100644
@@ -5046,10 +5046,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)
@@ -5082,6 +5141,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 f3ea36a231..b985b1ff72 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -12205,3 +12205,21 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+/*
index f9487417de..f3fa4c50ab 100644
@@ -2029,6 +2029,7 @@ typedef enum AlterTableType
index 1ab92a2afe..63117d5b92 100644
index 57804298df..d8371bdfd6 100644
@@ -143,6 +143,11 @@ extern int	partition_range_datum_bsearch(FmgrInfo *partsupfunc,
 
+									   RangeVar *splitPartName,

index 0000000000..08bf021796
@@ -0,0 +1,1417 @@
+-- ERROR:  lower bound of partition "sales_feb2022" is less than lower bound of split partition
+ERROR:  lower bound of partition "sales_feb2022" is less than lower bound of split partition
+-- ERROR:  upper bound of partition "sales_apr2022" is greater than upper bound of split partition
+ERROR:  upper bound of partition "sales_apr2022" is greater than upper bound of split partition
index d1924bb337..8b3876f48d 100644
 
index 0000000000..58e17f33e8
@@ -0,0 +1,833 @@
+-- ERROR:  lower bound of partition "sales_feb2022" is less than lower bound of split partition
+-- ERROR:  upper bound of partition "sales_apr2022" is greater than upper bound of split partition

---

From: vignesh C <[email-masked]>
Date: Fri, 26 Jan 2024 18:20:07 +0530

On Mon, 4 Dec 2023 at 13:22, Dmitry Koval <[email-masked]> wrote:
>
> Hello!
>
> Added commit v21-0004-SPLIT-PARTITION-optimization.patch.

CFBot shows that the patch does not apply anymore as in [1]:
=== Applying patches on top of PostgreSQL commit ID
8ba6fdf905d0f5aef70ced4504c6ad297bfe08ea ===
=== applying patch ./v21-0001-ALTER-TABLE-MERGE-PARTITIONS-command.patch
patching file src/backend/commands/tablecmds.c
...
Hunk #7 FAILED at 18735.
Hunk #8 succeeded at 20608 (offset 315 lines).
1 out of 8 hunks FAILED -- saving rejects to file
src/backend/commands/tablecmds.c.rej
patching file src/backend/parser/gram.y

Please post an updated version for the same.

[1] - http://cfbot.cputube.org/patch_46_3659.log

Regards,
Vignesh

---

From: Alvaro Herrera <[email-masked]>
Date: Fri, 26 Jan 2024 15:01:52 +0100

> Please post an updated version for the same.

Here's a rebase.  I only fixed the conflicts, didn't review.

---

From: Alvaro Herrera <[email-masked]>
Date: Fri, 26 Jan 2024 17:36:33 +0100

> Here's a rebase.  I only fixed the conflicts, didn't review.

Hmm, but I got the attached regression.diffs with it.  I didn't
investigate further, but it looks like the recent changes to replication
identity for partitioned tables has broken the regression tests.

-- 
Álvaro Herrera               48°01'N 7°57'E  —  https://www.EnterpriseDB.com/
"This is what I like so much about PostgreSQL.  Most of the surprises
are of the "oh wow!  That's cool" Not the "oh shit!" kind.  :)"
Scott Marlowe, http://archives.postgresql.org/pgsql-admin/2008-10/msg00152.php
diff -U3 /pgsql/source/master/src/test/regress/expected/partition_split.out /home/alvherre/Code/pgsql-build/master/src/test/regress/results/partition_split.out
--- /pgsql/source/master/src/test/regress/expected/partition_split.out	2024-01-26 14:57:39.549730792 +0100
+++ /home/alvherre/Code/pgsql-build/master/src/test/regress/results/partition_split.out	2024-01-26 15:22:15.007059433 +0100
@@ -777,8 +777,12 @@
 -- Create new partition with identity-column:
 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);
+ERROR:  table "salesmans2_5" being attached contains an identity column "salesman_id"
+DETAIL:  The new partition may not contain an identity column.
 INSERT INTO salesmans (salesman_name) VALUES ('Poirot');
 INSERT INTO salesmans (salesman_name) VALUES ('Ivanov');
+ERROR:  no partition of relation "salesmans" found for row
+DETAIL:  Partition key of the failing row contains (salesman_id) = (2).
 SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans'::regclass::oid;
     attname    | attidentity | attgenerated 
 ---------------+-------------+--------------
@@ -789,7 +793,7 @@
 SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans1_2'::regclass::oid;
     attname    | attidentity | attgenerated 
 ---------------+-------------+--------------
- salesman_id   |             | 
+ salesman_id   | a           | 
  salesman_name |             | 
 (2 rows)
 
@@ -805,8 +809,13 @@
   (PARTITION salesmans2_3 FOR VALUES FROM (2) TO (3),
    PARTITION salesmans3_4 FOR VALUES FROM (3) TO (4),
    PARTITION salesmans4_5 FOR VALUES FROM (4) TO (5));
+ERROR:  partition bound for relation "salesmans2_5" is null
 INSERT INTO salesmans (salesman_name) VALUES ('May');
+ERROR:  no partition of relation "salesmans" found for row
+DETAIL:  Partition key of the failing row contains (salesman_id) = (3).
 INSERT INTO salesmans (salesman_name) VALUES ('Ford');
+ERROR:  no partition of relation "salesmans" found for row
+DETAIL:  Partition key of the failing row contains (salesman_id) = (4).
 SELECT * FROM salesmans1_2;
  salesman_id | salesman_name 
 -------------+---------------
@@ -814,23 +823,17 @@
 (1 row)
 
 SELECT * FROM salesmans2_3;
- salesman_id | salesman_name 
--------------+---------------
-           2 | Ivanov
-(1 row)
-
+ERROR:  relation "salesmans2_3" does not exist
+LINE 1: SELECT * FROM salesmans2_3;
+                      ^
 SELECT * FROM salesmans3_4;
- salesman_id | salesman_name 
--------------+---------------
-           3 | May
-(1 row)
-
+ERROR:  relation "salesmans3_4" does not exist
+LINE 1: SELECT * FROM salesmans3_4;
+                      ^
 SELECT * FROM salesmans4_5;
- salesman_id | salesman_name 
--------------+---------------
-           4 | Ford
-(1 row)
-
+ERROR:  relation "salesmans4_5" does not exist
+LINE 1: SELECT * FROM salesmans4_5;
+                      ^
 SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans'::regclass::oid;
     attname    | attidentity | attgenerated 
 ---------------+-------------+--------------
@@ -841,32 +844,23 @@
 SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans1_2'::regclass::oid;
     attname    | attidentity | attgenerated 
 ---------------+-------------+--------------
- salesman_id   |             | 
+ salesman_id   | a           | 
  salesman_name |             | 
 (2 rows)
 
 -- New partitions have identity-columns:
 SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans2_3'::regclass::oid;
-    attname    | attidentity | attgenerated 
----------------+-------------+--------------
- salesman_id   | a           | 
- salesman_name |             | 
-(2 rows)
-
+ERROR:  relation "salesmans2_3" does not exist
+LINE 1: ...FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans...
+                                                             ^
 SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans3_4'::regclass::oid;
-    attname    | attidentity | attgenerated 
----------------+-------------+--------------
- salesman_id   | a           | 
- salesman_name |             | 
-(2 rows)
-
+ERROR:  relation "salesmans3_4" does not exist
+LINE 1: ...FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans...
+                                                             ^
 SELECT attname, attidentity, attgenerated FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans4_5'::regclass::oid;
-    attname    | attidentity | attgenerated 
----------------+-------------+--------------
- salesman_id   | a           | 
- salesman_name |             | 
-(2 rows)
-
+ERROR:  relation "salesmans4_5" does not exist
+LINE 1: ...FROM pg_attribute WHERE attnum > 0 AND attrelid = 'salesmans...
+                                                             ^
 DROP TABLE salesmans CASCADE;
 --
 -- Test: split partition with deleted columns
@@ -1121,19 +1115,20 @@
   (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:  no owned sequence found
 SELECT * FROM sales_list;
  salesman_id | salesman_name |  sales_state   | sales_amount | sales_date 
 -------------+---------------+----------------+--------------+------------
+           1 | Trump         | Magadan        |         1000 | 03-01-2022
            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
+           5 | Deev          | Voronezh       |          250 | 03-07-2022
            6 | Poirot        | Kazan          |         1000 | 03-01-2022
+           8 | Li            | Vladivostok    |         1150 | 03-09-2022
+          11 | Muller        | Bryansk        |          650 | 03-05-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
+          14 | Plato         | Voronezh       |          950 | 03-05-2022
            3 | Ford          | St. Petersburg |         2000 | 03-05-2022
            7 | May           | Ukhta          |         1200 | 03-06-2022
            9 | May           | Ukhta          |         1200 | 03-11-2022
@@ -1141,21 +1136,13 @@
 (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)
-
+ERROR:  relation "sales_west" does not exist
+LINE 1: SELECT * FROM sales_west;
+                      ^
 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)
-
+ERROR:  relation "sales_east" does not exist
+LINE 1: SELECT * FROM sales_east;
+                      ^
 SELECT * FROM sales_nord;
  salesman_id | salesman_name |  sales_state   | sales_amount | sales_date 
 -------------+---------------+----------------+--------------+------------
@@ -1166,24 +1153,16 @@
 (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)
-
+ERROR:  relation "sales_central" does not exist
+LINE 1: 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';
- 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)
-
+ERROR:  relation "sales_central" does not exist
+LINE 1: SELECT * FROM sales_central WHERE sales_state = 'Moscow';
+                      ^
 SELECT * FROM sales_list WHERE sales_state = 'Moscow';
  salesman_id | salesman_name | sales_state | sales_amount | sales_date 
 -------------+---------------+-------------+--------------+------------
@@ -1722,3 +1701,6 @@
 DROP TABLE test_2colkey CASCADE;
 --
 DROP SCHEMA partition_split_schema;
+ERROR:  cannot drop schema partition_split_schema because other objects depend on it
+DETAIL:  table salesmans2_5 depends on schema partition_split_schema
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.

---

From: Dmitry Koval <[email-masked]>
Date: Fri, 26 Jan 2024 20:08:08 +0300

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

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

---
 .../test_ddl_deparse/test_ddl_deparse.c       |   3 +
 15 files changed, 2100 insertions(+), 19 deletions(-)

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);

 /* ----------------------------------------------------------------
  *		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);
 
-	CreateInheritance(attachrel, rel, true);
-	AttachPartitionEnsureIndexes(wqueue, rel, attachrel);
 
 	/*
 	 * Generate partition constraint from the partition bound specification.
@@ -20584,3 +20621,283 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
index 130f7fc7c3..4a5b13c300 100644
@@ -744,7 +744,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -2299,6 +2299,7 @@ partition_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:
@@ -2342,6 +2359,7 @@ index_partition_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,
 }

+/*
@@ -3674,6 +3750,19 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,

 				/*
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)
 
index 476d55dd24..edfe36bf42 100644
@@ -924,6 +924,17 @@ typedef struct PartitionRangeDatum
 
+/*
@@ -932,6 +943,7 @@ typedef struct PartitionCmd
 
@@ -2248,6 +2260,7 @@ typedef enum AlterTableType
index 2331acac09..985474df89 100644
@@ -324,6 +324,7 @@ PG_KEYWORD("parameter", PARAMETER, UNRESERVED_KEYWORD, BARE_LABEL)
index 3d9cc1031f..0329d7bd84 100644
 
index b2be88ead1..1071256182 100644
@@ -103,6 +103,7 @@ test: partition-key-update-1
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)
 				strtype = "DETACH PARTITION ... FINALIZE";
+				strtype = "MERGE PARTITIONS";
 				strtype = "ADD IDENTITY";
index 1d8a414eea..6f0236d207 100644
@@ -119,7 +119,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
-test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate
+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
index 7e866e3c3d..e827ed0214 100644
@@ -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-masked]>
Date: Mon, 19 Sep 2022 17:58:52 +0300
Subject: [PATCH v23 2/4] ALTER TABLE SPLIT PARTITION command

---
 .../test_ddl_deparse/test_ddl_deparse.c       |    3 +
 src/test/regress/expected/partition_split.out | 1317 +++++++++++++++++
 src/test/regress/sql/partition_split.sql      |  792 ++++++++++
 16 files changed, 3536 insertions(+), 12 deletions(-)

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);

@@ -4735,6 +4738,10 @@ AlterTableGetLockLevel(List *cmds)
 				cmd_lockmode = ShareUpdateExclusiveLock;
 				break;
 
@@ -5164,6 +5171,11 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
@@ -5559,6 +5571,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
@@ -6555,6 +6575,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -20622,6 +20644,255 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 	return cstorage;
 }
 
+/*
@@ -20676,6 +20947,142 @@ createPartitionTable(RangeVar *newPartName, RangeVar *modelRelName,
 				   NULL);
 }
 
+/*
index 4a5b13c300..6da1f9a076 100644
@@ -643,6 +644,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -759,7 +762,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	SEQUENCE SEQUENCES
 
@@ -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,
 }

+/*
@@ -3742,7 +3779,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3750,6 +3787,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
@@ -3758,7 +3796,11 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
@@ -4163,13 +4205,13 @@ setSchemaName(const char *context_schema, char **stmt_schema_name)
 
 /*
  * transformPartitionCmd
 
@@ -4178,9 +4220,9 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 
@@ -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,
 	}
 }
 
+/*
@@ -5077,6 +5136,602 @@ get_partition_bound_spec(Oid partOid, RangeVar *name)
 	return boundspec;
 }
 
+/*
index b625f471a8..110c267823 100644
@@ -12653,3 +12653,21 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+/*
index edfe36bf42..d0ffa3e9de 100644
@@ -2260,6 +2260,7 @@ typedef enum AlterTableType
index 985474df89..89e0a5c504 100644
@@ -404,6 +404,7 @@ PG_KEYWORD("skip", SKIP, UNRESERVED_KEYWORD, BARE_LABEL)
index 0329d7bd84..9d2a26705f 100644
 
+extern void check_partitions_for_split(Relation parent,
+									   Oid splitPartOid,
+									   RangeVar *splitPartName,
+									   List *partlist,
+									   ParseState *pstate);
 extern void calculate_partition_bound_for_merge(Relation parent,
 												List *partNames,
 												List *partOids,
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 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);

index 1071256182..79b730fb86 100644
@@ -104,6 +104,7 @@ test: partition-key-update-2
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)
 				strtype = "DETACH PARTITION ... FINALIZE";
+				strtype = "SPLIT PARTITION";
 				strtype = "MERGE PARTITIONS";
index 0000000000..38736d3e67
@@ -0,0 +1,1317 @@
+(salesman_id SERIAL,
index 6f0236d207..117d78cb1b 100644
@@ -119,7 +119,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
-test: partition_merge partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate
+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
index 0000000000..e33e616230
@@ -0,0 +1,792 @@
+(salesman_id SERIAL,
2.40.1.windows.1

From 383fdf8857a9d36bd5350c06d0dfbbe02499504c Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email-masked]>
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-masked]>
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

---

From: Dmitry Koval <[email-masked]>
Date: Fri, 26 Jan 2024 21:36:59 +0300

I thought it's wrong to exclude the IDENTITY-column test, so I fixed the 
test and return it back.
Changes in attachment (commit 
v24-0002-ALTER-TABLE-SPLIT-PARTITION-command.patch).

-- 
With best regards,
Dmitry Koval

From 3041dee1fa86363dbaed12cde1d38f061f5fda1b Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email-masked]>
Date: Mon, 19 Sep 2022 16:06:03 +0300
Subject: [PATCH v24 1/4] ALTER TABLE MERGE PARTITIONS command

---

 

+/*

 	/*
 
 	return cstorage;
 }
+
+/*

 

+/*

 				/*

+/*

 

From 1db0a789f5b714ff6096d0aee043bfc081c2d552 Mon Sep 17 00:00:00 2001
From: Koval Dmitry <[email-masked]>
Date: Mon, 19 Sep 2022 17:58:52 +0300
Subject: [PATCH v24 2/4] ALTER TABLE SPLIT PARTITION command

---
 16 files changed, 3677 insertions(+), 12 deletions(-)

 

+/*
 
+/*

 
+partitions_list:

+/*

 
 /*
  * transformPartitionCmd

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

 
index 0000000000..7eb52f6fee
+CREATE TABLE salesmans2_5(salesman_id INT NOT NULL, salesman_name VARCHAR(30));
 
index 0000000000..40c71889b6
+CREATE TABLE salesmans2_5(salesman_id INT NOT NULL, salesman_name VARCHAR(30));

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

---

 

   <para>
 
+  <para>
 
  <refsect1>
-- 
2.40.1.windows.1

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

---

+/*
 
 /*

+/*
 
 /*

 	pfree(pc);
 }
 
 /*

 

 
 
 			pc->partqualstate =

 

 	/*

 	/*

 
 
 		if (tuple_map)

 
 
 /*

 
 	/*

 
 /*

 

 		/*

 

 

 /*
 
 		/*
index 7eb52f6fee..a8f3d8d6d1 100644
@@ -1414,4 +1414,311 @@ SELECT * FROM sales_others;
 
 DROP TABLE sales_range;
index 40c71889b6..17341353e0 100644
@@ -829,5 +829,157 @@ SELECT * FROM sales_others;
 
 DROP TABLE sales_range;
 
+--
+--