Implement ALTER TABLE ... SPLIT PARTITION ... command

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

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

Discussion: https://postgr.es/m/c73a1746-0cd0-6bdd-6b23-3ae0b7c0c582%40postgrespro.ru
Author: Dmitry Koval <d.koval@postgrespro.ru>
Co-authored-by: Alexander Korotkov <aekorotkov@gmail.com>
Co-authored-by: Tender Wang <tndrwang@gmail.com>
Co-authored-by: Richard Guo <guofenglinux@gmail.com>
Co-authored-by: Dagfinn Ilmari Mannsaker <ilmari@ilmari.org>
Co-authored-by: Fujii Masao <masao.fujii@gmail.com>
Co-authored-by: Jian He <jian.universality@gmail.com>
Reviewed-by: Matthias van de Meent <boekewurm+postgres@gmail.com>
Reviewed-by: Laurenz Albe <laurenz.albe@cybertec.at>
Reviewed-by: Zhihong Yu <zyu@yugabyte.com>
Reviewed-by: Justin Pryzby <pryzby@telsasoft.com>
Reviewed-by: Alvaro Herrera <alvherre@alvh.no-ip.org>
Reviewed-by: Robert Haas <rhaas@postgresql.org>
Reviewed-by: Stephane Tachoires <stephane.tachoires@gmail.com>
Reviewed-by: Jian He <jian.universality@gmail.com>
Reviewed-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Pavel Borisov <pashkin.elfe@gmail.com>
Reviewed-by: Masahiko Sawada <sawada.mshk@gmail.com>
Reviewed-by: Alexander Lakhin <exclusion@gmail.com>
Reviewed-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Reviewed-by: Daniel Gustafsson <dgustafsson@postgresql.org>
Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
Reviewed-by: Noah Misch <noah@leadboat.com>
pull/258/head
Alexander Korotkov 4 days ago
parent f2e4cc4279
commit 4b3d173629
  1. 19
      doc/src/sgml/ddl.sgml
  2. 118
      doc/src/sgml/ref/alter_table.sgml
  3. 432
      src/backend/commands/tablecmds.c
  4. 38
      src/backend/parser/gram.y
  5. 197
      src/backend/parser/parse_utilcmd.c
  6. 737
      src/backend/partitioning/partbounds.c
  7. 10
      src/bin/psql/tab-complete.in.c
  8. 34
      src/include/nodes/parsenodes.h
  9. 1
      src/include/parser/kwlist.h
  10. 4
      src/include/partitioning/partbounds.h
  11. 230
      src/test/isolation/expected/partition-split.out
  12. 1
      src/test/isolation/isolation_schedule
  13. 62
      src/test/isolation/specs/partition-split.spec
  14. 5
      src/test/modules/test_ddl_deparse/expected/alter_table.out
  15. 4
      src/test/modules/test_ddl_deparse/sql/alter_table.sql
  16. 3
      src/test/modules/test_ddl_deparse/test_ddl_deparse.c
  17. 1592
      src/test/regress/expected/partition_split.out
  18. 2
      src/test/regress/parallel_schedule
  19. 1134
      src/test/regress/sql/partition_split.sql
  20. 2
      src/tools/pgindent/typedefs.list

@ -4764,6 +4764,25 @@ ALTER TABLE measurement
measurement_y2006m03) INTO measurement_y2006q1; measurement_y2006m03) INTO measurement_y2006q1;
</programlisting> </programlisting>
</para> </para>
<para>
Similarly to merging multiple table partitions, there is an option for
splitting a single partition into multiple using the
<link linkend="sql-altertable-split-partition"><command>ALTER TABLE ... SPLIT PARTITION</command></link>.
This feature could come in handy when one partition grows too big
and needs to be split into multiple. It's important to note that
this operation is not supported for hash-partitioned tables and acquires
an <literal>ACCESS EXCLUSIVE</literal> lock, which could impact high-load
systems due to the lock's restrictive nature. For example, we can split
the quarter partition back to monthly partitions:
<programlisting>
ALTER TABLE measurement SPLIT PARTITION measurement_y2006q1 INTO
(PARTITION measurement_y2006m01 FOR VALUES FROM ('2006-01-01') TO ('2006-02-01'),
PARTITION measurement_y2006m02 FOR VALUES FROM ('2006-02-01') TO ('2006-03-01'),
PARTITION measurement_y2006m03 FOR VALUES FROM ('2006-03-01') TO ('2006-04-01'));
</programlisting>
</para>
</sect3> </sect3>
<sect3 id="ddl-partitioning-declarative-limitations"> <sect3 id="ddl-partitioning-declarative-limitations">

@ -39,6 +39,10 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
DETACH PARTITION <replaceable class="parameter">partition_name</replaceable> [ CONCURRENTLY | FINALIZE ] DETACH PARTITION <replaceable class="parameter">partition_name</replaceable> [ CONCURRENTLY | FINALIZE ]
ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> 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> MERGE PARTITIONS (<replaceable class="parameter">partition_name1</replaceable>, <replaceable class="parameter">partition_name2</replaceable> [, ...]) INTO <replaceable class="parameter">partition_name</replaceable>
ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
SPLIT PARTITION <replaceable class="parameter">partition_name</replaceable> INTO
(PARTITION <replaceable class="parameter">partition_name1</replaceable> { FOR VALUES <replaceable class="parameter">partition_bound_spec</replaceable> | DEFAULT },
PARTITION <replaceable class="parameter">partition_name2</replaceable> { FOR VALUES <replaceable class="parameter">partition_bound_spec</replaceable> | DEFAULT } [, ...])
<phrase>where <replaceable class="parameter">action</replaceable> is one of:</phrase> <phrase>where <replaceable class="parameter">action</replaceable> is one of:</phrase>
@ -1258,6 +1262,94 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem> </listitem>
</varlistentry> </varlistentry>
<varlistentry id="sql-altertable-split-partition">
<term>
<literal>SPLIT PARTITION <replaceable class="parameter">partition_name</replaceable> INTO (
PARTITION <replaceable class="parameter">partition_name1</replaceable> { FOR VALUES <replaceable class="parameter">partition_bound_spec</replaceable> | DEFAULT },
PARTITION <replaceable class="parameter">partition_name2</replaceable> { FOR VALUES <replaceable class="parameter">partition_bound_spec</replaceable> | DEFAULT }
[, ...])</literal>
</term>
<listitem>
<para>
This form splits a single partition of the target table into new
partitions. Hash-partitioned target table is not supported.
Only a simple, non-partitioned partition can be split.
If the split partition is the <literal>DEFAULT</literal> partition,
one of the new partitions must be <literal>DEFAULT</literal>.
If the partitioned table does not have a <literal>DEFAULT</literal>
partition, a <literal>DEFAULT</literal> partition can be defined as one
of the new partitions.
</para>
<para>
The bounds of new partitions should not overlap with those of new or
existing partitions (except <replaceable class="parameter">partition_name</replaceable>).
The combined bounds of new partitions <literal>
<replaceable class="parameter">partition_name1</replaceable>,
<replaceable class="parameter">partition_name2</replaceable>[, ...]
</literal> should be equal to the bounds of the split partition
<replaceable class="parameter">partition_name</replaceable>.
One of the new partitions can have the same name as the split partition
<replaceable class="parameter">partition_name</replaceable>
(this is suitable in case of splitting the <literal>DEFAULT</literal>
partition: after the split, the <literal>DEFAULT</literal> partition
remains with the same name, but its partition bound changes).
</para>
<para>
New partitions will have the same owner as the parent partition.
It is the user's responsibility to setup <acronym>ACL</acronym> on new
partitions.
</para>
<para>
<command>ALTER TABLE SPLIT PARTITION</command> uses the partitioned
table itself as the template to construct new partitions.
New partitions will inherit the same table access method, persistence
type, and tablespace as the partitioned table.
</para>
<para>
Constraints, column defaults, column generation expressions,
identity columns, indexes, and triggers are copied from the partitioned
table to the new partitions. But extended statistics, security
policies, etc, won't be copied from the partitioned table.
Indexes and identity columns copied from the partitioned table will be
created afterward, once the data has been moved into the new partitions.
</para>
<para>
When a partition is split, any objects that depend on this partition,
such as constraints, triggers, extended statistics, etc, will be dropped.
This occurs because <command>ALTER TABLE SPLIT PARTITION</command> uses
the partitioned table itself as the template to reconstruct these
objects later.
Eventually, we will drop the split partition
(using <literal>RESTRICT</literal> mode) too; therefore, if any objects
are still dependent on it, <command>ALTER TABLE SPLIT PARTITION</command>
would fail (see <xref linkend="ddl-depend"/>).
</para>
<note>
<para>
Split partition acquires an <literal>ACCESS EXCLUSIVE</literal> lock on
the parent table, in addition to the <literal>ACCESS EXCLUSIVE</literal>
lock on the table being split.
</para>
</note>
<note>
<para>
<command>ALTER TABLE SPLIT PARTITION</command> creates new partitions and
moves data from the split partition into them, which can take a long
time. So it is not recommended to use the command for splitting a
small fraction of rows out of a very big partition.
</para>
</note>
</listitem>
</varlistentry>
</variablelist> </variablelist>
</para> </para>
@ -1265,7 +1357,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
All the forms of <command>ALTER TABLE</command> that act on a single table, All the forms of <command>ALTER TABLE</command> that act on a single table,
except <literal>RENAME</literal>, <literal>SET SCHEMA</literal>, except <literal>RENAME</literal>, <literal>SET SCHEMA</literal>,
<literal>ATTACH PARTITION</literal>, <literal>DETACH PARTITION</literal>, <literal>ATTACH PARTITION</literal>, <literal>DETACH PARTITION</literal>,
and <literal>MERGE PARTITIONS</literal>, can be combined into <literal>MERGE PARTITIONS</literal>, and <literal>SPLIT PARTITION</literal>
can be combined into
a list of multiple alterations to be applied together. For example, it 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 is possible to add several columns and/or alter the type of several
columns in a single command. This is particularly useful with large columns in a single command. This is particularly useful with large
@ -1509,7 +1602,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<listitem> <listitem>
<para> <para>
The name of the table to attach as a new partition or to detach from this table, The name of the table to attach as a new partition or to detach from this table,
or the name of the new merged partition. or the name of split partition, or the name of the new merged partition.
</para> </para>
</listitem> </listitem>
</varlistentry> </varlistentry>
@ -1519,7 +1612,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<term><replaceable class="parameter">partition_name2</replaceable></term> <term><replaceable class="parameter">partition_name2</replaceable></term>
<listitem> <listitem>
<para> <para>
The names of the tables being merged into the new partition. The names of the tables being merged into the new partition or split into
new partitions.
</para> </para>
</listitem> </listitem>
</varlistentry> </varlistentry>
@ -1952,6 +2046,24 @@ ALTER TABLE measurement
DETACH PARTITION measurement_y2015m12; DETACH PARTITION measurement_y2015m12;
</programlisting></para> </programlisting></para>
<para>
To split a single partition of the range-partitioned table:
<programlisting>
ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2023 INTO
(PARTITION sales_feb2023 FOR VALUES FROM ('2023-02-01') TO ('2023-03-01'),
PARTITION sales_mar2023 FOR VALUES FROM ('2023-03-01') TO ('2023-04-01'),
PARTITION sales_apr2023 FOR VALUES FROM ('2023-04-01') TO ('2023-05-01'));
</programlisting></para>
<para>
To split a single partition of the list-partitioned table:
<programlisting>
ALTER TABLE sales_list SPLIT PARTITION sales_all INTO
(PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'),
PARTITION sales_east FOR VALUES IN ('Bejing', 'Delhi', 'Vladivostok'),
PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'));
</programlisting></para>
<para> <para>
To merge several partitions into one partition of the target table: To merge several partitions into one partition of the target table:
<programlisting> <programlisting>

@ -742,6 +742,9 @@ static char GetAttributeStorage(Oid atttypid, const char *storagemode);
static void ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel, static void ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
PartitionCmd *cmd, AlterTableUtilityContext *context); PartitionCmd *cmd, AlterTableUtilityContext *context);
static void ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab,
Relation rel, PartitionCmd *cmd,
AlterTableUtilityContext *context);
/* ---------------------------------------------------------------- /* ----------------------------------------------------------------
* DefineRelation * DefineRelation
@ -4839,6 +4842,7 @@ AlterTableGetLockLevel(List *cmds)
break; break;
case AT_MergePartitions: case AT_MergePartitions:
case AT_SplitPartition:
cmd_lockmode = AccessExclusiveLock; cmd_lockmode = AccessExclusiveLock;
break; break;
@ -5278,6 +5282,7 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
pass = AT_PASS_MISC; pass = AT_PASS_MISC;
break; break;
case AT_MergePartitions: case AT_MergePartitions:
case AT_SplitPartition:
ATSimplePermissions(cmd->subtype, rel, ATT_PARTITIONED_TABLE); ATSimplePermissions(cmd->subtype, rel, ATT_PARTITIONED_TABLE);
/* No command-specific prep needed */ /* No command-specific prep needed */
pass = AT_PASS_MISC; pass = AT_PASS_MISC;
@ -5686,6 +5691,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
ATExecMergePartitions(wqueue, tab, rel, (PartitionCmd *) cmd->def, ATExecMergePartitions(wqueue, tab, rel, (PartitionCmd *) cmd->def,
context); context);
break; 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;
default: /* oops */ default: /* oops */
elog(ERROR, "unrecognized alter table type: %d", elog(ERROR, "unrecognized alter table type: %d",
(int) cmd->subtype); (int) cmd->subtype);
@ -6728,6 +6741,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
return "DETACH PARTITION ... FINALIZE"; return "DETACH PARTITION ... FINALIZE";
case AT_MergePartitions: case AT_MergePartitions:
return "MERGE PARTITIONS"; return "MERGE PARTITIONS";
case AT_SplitPartition:
return "SPLIT PARTITION";
case AT_AddIdentity: case AT_AddIdentity:
return "ALTER COLUMN ... ADD IDENTITY"; return "ALTER COLUMN ... ADD IDENTITY";
case AT_SetIdentity: case AT_SetIdentity:
@ -22905,3 +22920,420 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
/* Restore the userid and security context. */ /* Restore the userid and security context. */
SetUserIdAndSecContext(save_userid, save_sec_context); SetUserIdAndSecContext(save_userid, save_sec_context);
} }
/*
* Struct with the context of the new partition for inserting rows from the
* split partition.
*/
typedef struct SplitPartitionContext
{
ExprState *partqualstate; /* expression for checking a slot for a
* partition (NULL for DEFAULT partition) */
BulkInsertState bistate; /* state of bulk inserts for partition */
TupleTableSlot *dstslot; /* slot for inserting row into partition */
AlteredTableInfo *tab; /* structure with generated column expressions
* and check constraint expressions. */
Relation partRel; /* relation for partition */
} SplitPartitionContext;
/*
* createSplitPartitionContext: create context for partition and fill it
*/
static SplitPartitionContext *
createSplitPartitionContext(Relation partRel)
{
SplitPartitionContext *pc;
pc = (SplitPartitionContext *) palloc0(sizeof(SplitPartitionContext));
pc->partRel = partRel;
/*
* Prepare a BulkInsertState for table_tuple_insert. The FSM is empty, so
* don't bother using it.
*/
pc->bistate = GetBulkInsertState();
/* Create a destination tuple slot for the new partition. */
pc->dstslot = table_slot_create(pc->partRel, NULL);
return pc;
}
/*
* deleteSplitPartitionContext: delete context for partition
*/
static void
deleteSplitPartitionContext(SplitPartitionContext *pc, List **wqueue, int ti_options)
{
ListCell *ltab;
ExecDropSingleTupleTableSlot(pc->dstslot);
FreeBulkInsertState(pc->bistate);
table_finish_bulk_insert(pc->partRel, ti_options);
/*
* We don't need to process this pc->partRel so delete the ALTER TABLE
* queue of it.
*/
foreach(ltab, *wqueue)
{
AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab);
if (tab->relid == RelationGetRelid(pc->partRel))
{
*wqueue = list_delete_cell(*wqueue, ltab);
break;
}
}
pfree(pc);
}
/*
* SplitPartitionMoveRows: scan split partition (splitRel) of partitioned table
* (rel) and move rows into new partitions.
*
* New partitions description:
* partlist: list of pointers to SinglePartitionSpec structures. It contains
* the partition specification details for all new partitions.
* newPartRels: list of Relations, new partitions created in
* ATExecSplitPartition.
*/
static void
SplitPartitionMoveRows(List **wqueue, Relation rel, Relation splitRel,
List *partlist, List *newPartRels)
{
/* 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;
mycid = GetCurrentCommandId(true);
estate = CreateExecutorState();
forboth(listptr, partlist, listptr2, newPartRels)
{
SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
pc = createSplitPartitionContext((Relation) lfirst(listptr2));
/* Find the work queue entry for the new partition table: newPartRel. */
pc->tab = ATGetQueueEntry(wqueue, pc->partRel);
buildExpressionExecutionStates(pc->tab, pc->partRel, estate);
if (sps->bound->is_default)
{
/*
* We should not create a structure to check the partition
* constraint for the new 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 a boolean expression for ExecCheck(). */
partConstraint = list_make1(make_ands_explicit(partConstraint));
/*
* Map the vars in the constraint expression from rel's attnos to
* splitRel's.
*/
partConstraint = map_partition_varattnos(partConstraint,
1, splitRel, rel);
pc->partqualstate =
ExecPrepareExpr((Expr *) linitial(partConstraint), estate);
Assert(pc->partqualstate != NULL);
}
/* Store partition context into a list. */
partContexts = lappend(partContexts, pc);
}
econtext = GetPerTupleExprContext(estate);
/* Create the necessary tuple slot. */
srcslot = table_slot_create(splitRel, NULL);
/*
* Map computing for moving attributes of the split partition to the new
* partition (for the first new partition, but other new partitions can
* use the same map).
*/
pc = (SplitPartitionContext *) lfirst(list_head(partContexts));
tuple_map = convert_tuples_by_name(RelationGetDescr(splitRel),
RelationGetDescr(pc->partRel));
/* Scan through the rows. */
snapshot = RegisterSnapshot(GetLatestSnapshot());
scan = table_beginscan(splitRel, snapshot, 0, NULL);
/*
* Switch to per-tuple memory context and reset it for each tuple
* produced, so we don't leak memory.
*/
oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot))
{
bool found = false;
TupleTableSlot *insertslot;
CHECK_FOR_INTERRUPTS();
econtext->ecxt_scantuple = srcslot;
/* Search partition for the current slot, srcslot. */
foreach(listptr, partContexts)
{
pc = (SplitPartitionContext *) lfirst(listptr);
/* skip DEFAULT partition */
if (pc->partqualstate && ExecCheck(pc->partqualstate, econtext))
{
found = true;
break;
}
}
if (!found)
{
/* Use the 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 a map to copy attributes. */
insertslot = execute_attr_map_slot(tuple_map->attrMap, srcslot, pc->dstslot);
}
else
{
/* Extract data from the old tuple. */
slot_getallattrs(srcslot);
/* 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);
}
/*
* Constraints and GENERATED expressions might reference the tableoid
* column, so fill tts_tableOid with the desired value. (We must do
* this each time, because it gets overwritten with newrel's OID
* during storing.)
*/
insertslot->tts_tableOid = RelationGetRelid(pc->partRel);
/*
* Now, evaluate any generated expressions whose inputs come from the
* new tuple. We assume these columns won't reference each other, so
* that there's no ordering dependency.
*/
evaluateGeneratedExpressionsAndCheckConstraints(pc->tab, pc->partRel,
insertslot, econtext);
/* Write the tuple out to the new relation. */
table_tuple_insert(pc->partRel, insertslot, mycid,
ti_options, pc->bistate);
ResetExprContext(econtext);
}
MemoryContextSwitchTo(oldCxt);
table_endscan(scan);
UnregisterSnapshot(snapshot);
if (tuple_map)
free_conversion_map(tuple_map);
ExecDropSingleTupleTableSlot(srcslot);
FreeExecutorState(estate);
foreach_ptr(SplitPartitionContext, spc, partContexts)
deleteSplitPartitionContext(spc, wqueue, ti_options);
}
/*
* 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;
ListCell *listptr,
*listptr2;
bool isSameName = false;
char tmpRelName[NAMEDATALEN];
List *newPartRels = NIL;
ObjectAddress object;
Oid defaultPartOid;
Oid save_userid;
int save_sec_context;
int save_nestlevel;
defaultPartOid = get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true));
/*
* Partition is already locked in the transformPartitionCmdForSplit
* function.
*/
splitRel = table_openrv(cmd->name, NoLock);
splitRelOid = RelationGetRelid(splitRel);
/* Check descriptions of new partitions. */
foreach_node(SinglePartitionSpec, sps, cmd->partlist)
{
Oid existingRelid;
/* Look up the existing relation by the new partition name. */
RangeVarGetAndCheckCreationNamespace(sps->name, NoLock, &existingRelid);
/*
* This would fail later on anyway if the relation already exists. But
* by catching it here, we can emit a nicer error message.
*/
if (existingRelid == splitRelOid && !isSameName)
/* One new partition can have the same name as a split partition. */
isSameName = true;
else if (OidIsValid(existingRelid))
ereport(ERROR,
errcode(ERRCODE_DUPLICATE_TABLE),
errmsg("relation \"%s\" already exists", sps->name->relname));
}
/* Detach the split partition. */
detachPartitionTable(rel, splitRel, defaultPartOid);
/*
* Perform a preliminary check to determine whether it's safe to drop the
* split partition before we actually do so later. After merging rows into
* the new partitions via SplitPartitionMoveRows, all old partitions need
* to be dropped. However, since the drop behavior is DROP_RESTRICT and
* the merge process (SplitPartitionMoveRows) can be time-consuming,
* performing an early check on the drop eligibility of old partitions is
* preferable.
*/
object.objectId = splitRelOid;
object.classId = RelationRelationId;
object.objectSubId = 0;
performDeletionCheck(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
/*
* If a new partition has the same name as the split partition, then we
* should rename the split partition to reuse its name.
*/
if (isSameName)
{
/*
* We must bump the command counter to make the split partition tuple
* visible for renaming.
*/
CommandCounterIncrement();
/* Rename partition. */
sprintf(tmpRelName, "split-%u-%X-tmp", RelationGetRelid(rel), MyProcPid);
RenameRelationInternal(splitRelOid, tmpRelName, true, false);
/*
* We must bump the command counter to make the split partition tuple
* visible after renaming.
*/
CommandCounterIncrement();
}
/* Create new partitions (like a split partition), without indexes. */
foreach_node(SinglePartitionSpec, sps, cmd->partlist)
{
Relation newPartRel;
newPartRel = createPartitionTable(wqueue, sps->name, rel,
splitRel->rd_rel->relowner);
newPartRels = lappend(newPartRels, newPartRel);
}
/*
* Switch to the table owner's userid, so that any index functions are run
* as that user. Also, lockdown security-restricted operations and
* arrange to make GUC variable changes local to this command.
*
* Need to do it after determining the namespace in the
* createPartitionTable() call.
*/
GetUserIdAndSecContext(&save_userid, &save_sec_context);
SetUserIdAndSecContext(splitRel->rd_rel->relowner,
save_sec_context | SECURITY_RESTRICTED_OPERATION);
save_nestlevel = NewGUCNestLevel();
RestrictSearchPath();
/* Copy data from the split partition to the new partitions. */
SplitPartitionMoveRows(wqueue, rel, splitRel, cmd->partlist, newPartRels);
/* Keep the lock until commit. */
table_close(splitRel, NoLock);
/* Attach new partitions to the partitioned table. */
forboth(listptr, cmd->partlist, listptr2, newPartRels)
{
SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
Relation newPartRel = (Relation) lfirst(listptr2);
/*
* wqueue = NULL: verification for each cloned constraint is not
* needed.
*/
attachPartitionTable(NULL, rel, newPartRel, sps->bound);
/* Keep the lock until commit. */
table_close(newPartRel, NoLock);
}
/* Drop the split partition. */
object.classId = RelationRelationId;
object.objectId = splitRelOid;
object.objectSubId = 0;
/* Probably DROP_CASCADE is not needed. */
performDeletion(&object, DROP_RESTRICT, 0);
/* Roll back any GUC changes executed by index functions. */
AtEOXact_GUC(false, save_nestlevel);
/* Restore the userid and security context. */
SetUserIdAndSecContext(save_userid, save_sec_context);
}

@ -262,6 +262,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
PartitionElem *partelem; PartitionElem *partelem;
PartitionSpec *partspec; PartitionSpec *partspec;
PartitionBoundSpec *partboundspec; PartitionBoundSpec *partboundspec;
SinglePartitionSpec *singlepartspec;
RoleSpec *rolespec; RoleSpec *rolespec;
PublicationObjSpec *publicationobjectspec; PublicationObjSpec *publicationobjectspec;
PublicationAllObjSpec *publicationallobjectspec; PublicationAllObjSpec *publicationallobjectspec;
@ -648,6 +649,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type <partelem> part_elem %type <partelem> part_elem
%type <list> part_params %type <list> part_params
%type <partboundspec> PartitionBoundSpec %type <partboundspec> PartitionBoundSpec
%type <singlepartspec> SinglePartitionSpec
%type <list> partitions_list
%type <list> hash_partbound %type <list> hash_partbound
%type <defelt> hash_partbound_elem %type <defelt> hash_partbound_elem
@ -777,7 +780,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
SAVEPOINT SCALAR SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SELECT SAVEPOINT SCALAR SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SELECT
SEQUENCE SEQUENCES SEQUENCE SEQUENCES
SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW
SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SOURCE SQL_P STABLE STANDALONE_P SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SPLIT SOURCE SQL_P STABLE STANDALONE_P
START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRING_P STRIP_P START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRING_P STRIP_P
SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER
@ -2385,6 +2388,23 @@ alter_table_cmds:
| alter_table_cmds ',' alter_table_cmd { $$ = lappend($1, $3); } | 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: partition_cmd:
/* ALTER TABLE <name> ATTACH PARTITION <table_name> FOR VALUES */ /* ALTER TABLE <name> ATTACH PARTITION <table_name> FOR VALUES */
ATTACH PARTITION qualified_name PartitionBoundSpec ATTACH PARTITION qualified_name PartitionBoundSpec
@ -2429,6 +2449,20 @@ partition_cmd:
n->def = (Node *) cmd; n->def = (Node *) cmd;
$$ = (Node *) n; $$ = (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> */ /* ALTER TABLE <name> MERGE PARTITIONS () INTO <partition_name> */
| MERGE PARTITIONS '(' qualified_name_list ')' INTO qualified_name | MERGE PARTITIONS '(' qualified_name_list ')' INTO qualified_name
{ {
@ -18126,6 +18160,7 @@ unreserved_keyword:
| SKIP | SKIP
| SNAPSHOT | SNAPSHOT
| SOURCE | SOURCE
| SPLIT
| SQL_P | SQL_P
| STABLE | STABLE
| STANDALONE_P | STANDALONE_P
@ -18768,6 +18803,7 @@ bare_label_keyword:
| SNAPSHOT | SNAPSHOT
| SOME | SOME
| SOURCE | SOURCE
| SPLIT
| SQL_P | SQL_P
| STABLE | STABLE
| STANDALONE_P | STANDALONE_P

@ -137,7 +137,7 @@ static void transformConstraintAttrs(CreateStmtContext *cxt,
List *constraintList); List *constraintList);
static void transformColumnType(CreateStmtContext *cxt, ColumnDef *column); static void transformColumnType(CreateStmtContext *cxt, ColumnDef *column);
static void setSchemaName(const char *context_schema, char **stmt_schema_name); 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, static List *transformPartitionRangeBounds(ParseState *pstate, List *blist,
Relation parent); Relation parent);
static void validateInfiniteBounds(ParseState *pstate, List *blist); static void validateInfiniteBounds(ParseState *pstate, List *blist);
@ -3515,9 +3515,11 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
/* /*
* checkPartition * checkPartition
* Check whether partRelOid is a leaf partition of the parent table (rel). * Check whether partRelOid is a leaf partition of the parent table (rel).
* isMerge: true indicates the operation is "ALTER TABLE ... MERGE PARTITIONS";
* false indicates the operation is "ALTER TABLE ... SPLIT PARTITION".
*/ */
static void static void
checkPartition(Relation rel, Oid partRelOid) checkPartition(Relation rel, Oid partRelOid, bool isMerge)
{ {
Relation partRel; Relation partRel;
@ -3527,25 +3529,176 @@ checkPartition(Relation rel, Oid partRelOid)
ereport(ERROR, ereport(ERROR,
errcode(ERRCODE_WRONG_OBJECT_TYPE), errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not a table", RelationGetRelationName(partRel)), errmsg("\"%s\" is not a table", RelationGetRelationName(partRel)),
errhint("ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions")); isMerge
? errhint("ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions")
: errhint("ALTER TABLE ... SPLIT PARTITION can only split partitions don't have sub-partitions"));
if (!partRel->rd_rel->relispartition) if (!partRel->rd_rel->relispartition)
ereport(ERROR, ereport(ERROR,
errcode(ERRCODE_WRONG_OBJECT_TYPE), errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not a partition of partitioned table \"%s\"", errmsg("\"%s\" is not a partition of partitioned table \"%s\"",
RelationGetRelationName(partRel), RelationGetRelationName(rel)), RelationGetRelationName(partRel), RelationGetRelationName(rel)),
errhint("ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions")); isMerge
? errhint("ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions")
: errhint("ALTER TABLE ... SPLIT PARTITION can only split partitions don't have sub-partitions"));
if (get_partition_parent(partRelOid, false) != RelationGetRelid(rel)) if (get_partition_parent(partRelOid, false) != RelationGetRelid(rel))
ereport(ERROR, ereport(ERROR,
errcode(ERRCODE_UNDEFINED_TABLE), errcode(ERRCODE_UNDEFINED_TABLE),
errmsg("relation \"%s\" is not a partition of relation \"%s\"", errmsg("relation \"%s\" is not a partition of relation \"%s\"",
RelationGetRelationName(partRel), RelationGetRelationName(rel)), RelationGetRelationName(partRel), RelationGetRelationName(rel)),
errhint("ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions")); isMerge
? errhint("ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions")
: errhint("ALTER TABLE ... SPLIT PARTITION can only split partitions don't have sub-partitions"));
table_close(partRel, NoLock); table_close(partRel, NoLock);
} }
/*
* transformPartitionCmdForSplit -
* analyze the ALTER TABLE ... SPLIT PARTITION command
*
* For each new partition, sps->bound is set to the transformed value of bound.
* Does checks for bounds of new partitions.
*/
static void
transformPartitionCmdForSplit(CreateStmtContext *cxt, PartitionCmd *partcmd)
{
Relation parent = cxt->rel;
PartitionKey key;
char strategy;
Oid splitPartOid;
Oid defaultPartOid;
int default_index = -1;
bool isSplitPartDefault;
ListCell *listptr,
*listptr2;
List *splitlist;
splitlist = partcmd->partlist;
key = RelationGetPartitionKey(parent);
strategy = get_partition_strategy(key);
defaultPartOid = get_default_oid_from_partdesc(RelationGetPartitionDesc(parent, true));
/* Transform partition bounds for all partitions in the list: */
foreach_node(SinglePartitionSpec, sps, splitlist)
{
cxt->partbound = NULL;
transformPartitionCmd(cxt, sps->bound);
/* Assign the transformed value of the partition bound. */
sps->bound = cxt->partbound;
}
/*
* Open and lock the partition, check ownership along the way. We need to
* use AccessExclusiveLock here because this split partition will be
* detached, then dropped in ATExecSplitPartition.
*/
splitPartOid = RangeVarGetRelidExtended(partcmd->name, AccessExclusiveLock,
0, RangeVarCallbackOwnsRelation,
NULL);
checkPartition(parent, splitPartOid, false);
switch (strategy)
{
case PARTITION_STRATEGY_LIST:
case PARTITION_STRATEGY_RANGE:
{
foreach_node(SinglePartitionSpec, sps, splitlist)
{
if (sps->bound->is_default)
{
if (default_index != -1)
ereport(ERROR,
errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("DEFAULT partition should be one"),
parser_errposition(cxt->pstate, sps->name->location));
default_index = foreach_current_index(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;
}
/* isSplitPartDefault: is the being split partition a DEFAULT partition? */
isSplitPartDefault = (defaultPartOid == splitPartOid);
if (isSplitPartDefault && default_index == -1)
ereport(ERROR,
errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("can not split DEFAULT partition \"%s\"",
get_rel_name(splitPartOid)),
errhint("To split DEFAULT partition one of the new partition msut be DEFAULT"),
parser_errposition(cxt->pstate, ((SinglePartitionSpec *) linitial(splitlist))->name->location));
/*
* If the partition being split is not the DEFAULT partition, but the
* DEFAULT partition exists, then none of the resulting split partitions
* can be the DEFAULT.
*/
if (!isSplitPartDefault && (default_index != -1) && OidIsValid(defaultPartOid))
{
SinglePartitionSpec *spsDef =
(SinglePartitionSpec *) list_nth(splitlist, default_index);
ereport(ERROR,
errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("can not split non-DEFAULT partition \"%s\"",
get_rel_name(splitPartOid)),
errmsg("new partition cannot be DEFAULT because DEFAULT partition \"%s\" already exists",
get_rel_name(defaultPartOid)),
parser_errposition(cxt->pstate, spsDef->name->location));
}
foreach(listptr, splitlist)
{
Oid nspid;
SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
RangeVar *name = sps->name;
nspid = RangeVarGetCreationNamespace(sps->name);
/* Partitions in the list should have different names. */
for_each_cell(listptr2, splitlist, lnext(splitlist, listptr))
{
Oid nspid2;
SinglePartitionSpec *sps2 = (SinglePartitionSpec *) lfirst(listptr2);
RangeVar *name2 = sps2->name;
if (equal(name, name2))
ereport(ERROR,
errcode(ERRCODE_DUPLICATE_TABLE),
errmsg("partition with name \"%s\" is already used", name->relname),
parser_errposition(cxt->pstate, name2->location));
nspid2 = RangeVarGetCreationNamespace(sps2->name);
if (nspid2 == nspid && strcmp(name->relname, name2->relname) == 0)
ereport(ERROR,
errcode(ERRCODE_DUPLICATE_TABLE),
errmsg("partition with name \"%s\" is already used", name->relname),
parser_errposition(cxt->pstate, name2->location));
}
}
/* Then we should check partitions with transformed bounds. */
check_partitions_for_split(parent, splitPartOid, splitlist, cxt->pstate);
}
/* /*
* transformPartitionCmdForMerge - * transformPartitionCmdForMerge -
* analyze the ALTER TABLE ... MERGE PARTITIONS command * analyze the ALTER TABLE ... MERGE PARTITIONS command
@ -3622,7 +3775,7 @@ transformPartitionCmdForMerge(CreateStmtContext *cxt, PartitionCmd *partcmd)
parser_errposition(cxt->pstate, name->location)); parser_errposition(cxt->pstate, name->location));
} }
checkPartition(parent, partOid); checkPartition(parent, partOid, true);
partOids = lappend_oid(partOids, partOid); partOids = lappend_oid(partOids, partOid);
} }
@ -3910,8 +4063,8 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
{ {
PartitionCmd *partcmd = (PartitionCmd *) cmd->def; PartitionCmd *partcmd = (PartitionCmd *) cmd->def;
transformPartitionCmd(&cxt, partcmd); transformPartitionCmd(&cxt, partcmd->bound);
/* assign transformed value of the partition bound */ /* assign the transformed value of the partition bound */
partcmd->bound = cxt.partbound; partcmd->bound = cxt.partbound;
} }
@ -3932,6 +4085,20 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
break; break;
} }
case AT_SplitPartition:
{
PartitionCmd *partcmd = (PartitionCmd *) cmd->def;
if (list_length(partcmd->partlist) < 2)
ereport(ERROR,
errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("list of new partitions should contain at least two partitions"));
transformPartitionCmdForSplit(&cxt, partcmd);
newcmds = lappend(newcmds, cmd);
break;
}
default: default:
/* /*
@ -4362,13 +4529,13 @@ setSchemaName(const char *context_schema, char **stmt_schema_name)
/* /*
* transformPartitionCmd * 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 * In case of the ATTACH/SPLIT PARTITION command, cxt->partbound is set to the
* transformed value of cmd->bound. * transformed value of bound.
*/ */
static void static void
transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd) transformPartitionCmd(CreateStmtContext *cxt, PartitionBoundSpec *bound)
{ {
Relation parentRel = cxt->rel; Relation parentRel = cxt->rel;
@ -4377,9 +4544,9 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
case RELKIND_PARTITIONED_TABLE: case RELKIND_PARTITIONED_TABLE:
/* transform the partition bound, if any */ /* transform the partition bound, if any */
Assert(RelationGetPartitionKey(parentRel) != NULL); Assert(RelationGetPartitionKey(parentRel) != NULL);
if (cmd->bound != NULL) if (bound != NULL)
cxt->partbound = transformPartitionBound(cxt->pstate, parentRel, cxt->partbound = transformPartitionBound(cxt->pstate, parentRel,
cmd->bound); bound);
break; break;
case RELKIND_PARTITIONED_INDEX: case RELKIND_PARTITIONED_INDEX:
@ -4387,7 +4554,7 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
* A partitioned index cannot have a partition bound set. ALTER * A partitioned index cannot have a partition bound set. ALTER
* INDEX prevents that with its grammar, but not ALTER TABLE. * INDEX prevents that with its grammar, but not ALTER TABLE.
*/ */
if (cmd->bound != NULL) if (bound != NULL)
ereport(ERROR, ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION), (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("\"%s\" is not a partitioned table", errmsg("\"%s\" is not a partitioned table",

@ -17,6 +17,7 @@
#include "access/relation.h" #include "access/relation.h"
#include "access/table.h" #include "access/table.h"
#include "access/tableam.h" #include "access/tableam.h"
#include "catalog/namespace.h"
#include "catalog/partition.h" #include "catalog/partition.h"
#include "catalog/pg_inherits.h" #include "catalog/pg_inherits.h"
#include "catalog/pg_type.h" #include "catalog/pg_type.h"
@ -4974,15 +4975,22 @@ satisfies_hash_partition(PG_FUNCTION_ARGS)
* *
* (function for BY RANGE partitioning) * (function for BY RANGE partitioning)
* *
* This is a helper function for calculate_partition_bound_for_merge(). This * This is a helper function for check_partitions_for_split() and
* function compares the upper bound of first_bound and the lower bound of * calculate_partition_bound_for_merge(). This function compares the upper
* second_bound. These bounds should be equal. * bound of first_bound and the lower bound of second_bound. These bounds
* should be equal except when "defaultPart == true" (this means that one of
* the split partitions is DEFAULT). In this case, the upper bound of
* first_bound can be less than the lower bound of second_bound because
* the space between these bounds will be included in the DEFAULT partition.
* *
* parent: partitioned table * parent: partitioned table
* first_name: name of the first partition * first_name: name of the first partition
* first_bound: bound of the first partition * first_bound: bound of the first partition
* second_name: name of the second partition * second_name: name of the second partition
* second_bound: bound of the second partition * second_bound: bound of the second partition
* defaultPart: true if one of the new partitions is DEFAULT
* is_merge: true ndicates the operation is MERGE PARTITIONS;
* false indicates the operation is SPLIT PARTITION.
* pstate: pointer to ParseState struct for determining error position * pstate: pointer to ParseState struct for determining error position
*/ */
static void static void
@ -4991,6 +4999,8 @@ check_two_partitions_bounds_range(Relation parent,
PartitionBoundSpec *first_bound, PartitionBoundSpec *first_bound,
RangeVar *second_name, RangeVar *second_name,
PartitionBoundSpec *second_bound, PartitionBoundSpec *second_bound,
bool defaultPart,
bool is_merge,
ParseState *pstate) ParseState *pstate)
{ {
PartitionKey key = RelationGetPartitionKey(parent); PartitionKey key = RelationGetPartitionKey(parent);
@ -5012,18 +5022,28 @@ check_two_partitions_bounds_range(Relation parent,
key->partcollation, key->partcollation,
second_lower->datums, second_lower->kind, second_lower->datums, second_lower->kind,
false, first_upper); false, first_upper);
if (cmpval) if ((!defaultPart && cmpval) || (defaultPart && cmpval < 0))
{ {
PartitionRangeDatum *datum = linitial(second_bound->lowerdatums); PartitionRangeDatum *datum = linitial(second_bound->lowerdatums);
ereport(ERROR, if (is_merge)
errcode(ERRCODE_INVALID_OBJECT_DEFINITION), ereport(ERROR,
errmsg("can not merge partition \"%s\" together with partition \"%s\"", errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
second_name->relname, first_name->relname), errmsg("can not merge partition \"%s\" together with partition \"%s\"",
errdetail("lower bound of partition \"%s\" is not equal to the upper bound of partition \"%s\"", second_name->relname, first_name->relname),
second_name->relname, first_name->relname), errdetail("lower bound of partition \"%s\" is not equal to the upper bound of partition \"%s\"",
errhint("ALTER TABLE ... MERGE PARTITIONS requires the partition bounds to be adjacent."), second_name->relname, first_name->relname),
parser_errposition(pstate, datum->location)); errhint("ALTER TABLE ... MERGE PARTITIONS requires the partition bounds to be adjacent."),
parser_errposition(pstate, datum->location));
else
ereport(ERROR,
errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("can not split to partition \"%s\" together with partition \"%s\"",
second_name->relname, first_name->relname),
errdetail("lower bound of partition \"%s\" is not equal to the upper bound of partition \"%s\"",
second_name->relname, first_name->relname),
errhint("ALTER TABLE ... SPLIT PARTITION requires the partition bounds to be adjacent."),
parser_errposition(pstate, datum->location));
} }
} }
@ -5126,6 +5146,8 @@ calculate_partition_bound_for_merge(Relation parent,
(PartitionBoundSpec *) list_nth(bounds, prev_index), (PartitionBoundSpec *) list_nth(bounds, prev_index),
(RangeVar *) list_nth(partNames, index), (RangeVar *) list_nth(partNames, index),
(PartitionBoundSpec *) list_nth(bounds, index), (PartitionBoundSpec *) list_nth(bounds, index),
false,
true,
pstate); pstate);
} }
@ -5164,3 +5186,694 @@ calculate_partition_bound_for_merge(Relation parent,
(int) key->strategy); (int) key->strategy);
} }
} }
/*
* partitions_listdatum_intersection
*
* (function for BY LIST partitioning)
*
* Function compares lists of values for different partitions.
* Return a list that contains *one* cell that is present in both list1 and
* list2. The returned list is freshly allocated via palloc(), but the
* cells themselves point to the same objects as the cells of the
* input lists.
*
* Currently, there is no need to collect all common partition datums from the
* two lists.
*/
static List *
partitions_listdatum_intersection(FmgrInfo *partsupfunc, Oid *partcollation,
const List *list1, const List *list2)
{
List *result = NIL;
if (list1 == NIL || list2 == NIL)
return result;
foreach_node(Const, val1, list1)
{
bool isnull1 = val1->constisnull;
foreach_node(Const, val2, list2)
{
if (val2->constisnull)
{
if (isnull1)
{
result = lappend(result, val1);
return result;
}
continue;
}
else if (isnull1)
continue;
/* Compare two datum values. */
if (DatumGetInt32(FunctionCall2Coll(&partsupfunc[0],
partcollation[0],
val1->constvalue,
val2->constvalue)) == 0)
{
result = lappend(result, val1);
return result;
}
}
}
return result;
}
/*
* 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 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];
overlap = partitions_listdatum_intersection(&key->partsupfunc[0],
key->partcollation,
sps1->bound->listdatums,
sps2->bound->listdatums);
if (list_length(overlap) > 0)
{
Const *val = (Const *) linitial_node(Const, overlap);
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, exprLocation((Node *) val)));
}
}
}
}
/*
* check_partition_bounds_for_split_range
*
* (function for BY RANGE partitioning)
*
* Checks that bounds of new partition "spec" are inside bounds of split
* partition (with Oid splitPartOid). If first=true (this means that "spec" is
* the first of the new partitions), then the lower bound of "spec" should be
* equal (or greater than or equal in case defaultPart=true) to the lower
* bound of the split partition. If last=true (this means that "spec" is the
* last of the new partitions), then the upper bound of "spec" should be
* equal (or less than or equal in case defaultPart=true) to the upper bound
* of the split partition.
*
* parent: partitioned table
* relname: name of the new partition
* spec: bounds specification of the new partition
* splitPartOid: split partition Oid
* first: true iff the new partition "spec" is the first of the
* new partitions
* last: true iff the new partition "spec" is the last of the
* new partitions
* defaultPart: true iff new partitions contain the DEFAULT partition
* pstate: pointer to ParseState struct to determine error position
*/
static void
check_partition_bounds_for_split_range(Relation parent,
char *relname,
PartitionBoundSpec *spec,
Oid splitPartOid,
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 the 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 the 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, exprLocation((Node *) datum)));
}
/*
* Need to check first and last partitions (from the set of new
* partitions)
*/
if (first || last)
{
PartitionBoundSpec *split_spec = get_partition_bound_spec(splitPartOid);
PartitionRangeDatum *datum;
if (first)
{
PartitionRangeBound *split_lower;
split_lower = make_one_partition_rbound(key, -1, split_spec->lowerdatums, true);
cmpval = partition_rbound_cmp(key->partnatts,
key->partsupfunc,
key->partcollation,
lower->datums, lower->kind,
true, split_lower);
if (cmpval != 0)
datum = list_nth(spec->lowerdatums, abs(cmpval) - 1);
/*
* The lower bound of "spec" must equal the lower bound of the
* split partition. However, if one of the new partitions is
* DEFAULT, then it is ok for the new partition's lower bound to
* be greater than that of the split partition.
*/
if (!defaultPart)
{
if (cmpval != 0)
ereport(ERROR,
errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("lower bound of partition \"%s\" is not equal to lower bound of split partition \"%s\"",
relname,
get_rel_name(splitPartOid)),
errhint("%s require combined bounds of new partitions must exactly match the bound of the split partition",
"ALTER TABLE ... SPLIT PARTITION"),
parser_errposition(pstate, exprLocation((Node *) datum)));
}
else if (cmpval < 0)
ereport(ERROR,
errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("lower bound of partition \"%s\" is less than lower bound of split partition \"%s\"",
relname,
get_rel_name(splitPartOid)),
errhint("%s require combined bounds of new partitions must exactly match the bound of the split partition",
"ALTER TABLE ... SPLIT PARTITION"),
parser_errposition(pstate, exprLocation((Node *) datum)));
}
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);
if (cmpval != 0)
datum = list_nth(spec->upperdatums, abs(cmpval) - 1);
/*
* The upper bound of "spec" must equal the upper bound of the
* split partition. However, if one of the new partitions is
* DEFAULT, then it is ok for the new partition's upper bound to
* be less than that of the split partition.
*/
if (!defaultPart)
{
if (cmpval != 0)
ereport(ERROR,
errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("upper bound of partition \"%s\" is not equal to upper bound of split partition \"%s\"",
relname,
get_rel_name(splitPartOid)),
errhint("%s require combined bounds of new partitions must exactly match the bound of the split partition",
"ALTER TABLE ... SPLIT PARTITION"),
parser_errposition(pstate, exprLocation((Node *) datum)));
}
else if (cmpval > 0)
ereport(ERROR,
errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("upper bound of partition \"%s\" is greater than upper bound of split partition \"%s\"",
relname,
get_rel_name(splitPartOid)),
errhint("%s require combined bounds of new partitions must exactly match the bound of the split partition",
"ALTER TABLE ... SPLIT PARTITION"),
parser_errposition(pstate, exprLocation((Node *) datum)));
}
}
}
/*
* check_partition_bounds_for_split_list
*
* (function for BY LIST partitioning)
*
* Checks that the bounds of the new partition are inside the bounds of the
* 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 to 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;
Assert(key->strategy == PARTITION_STRATEGY_LIST);
Assert(spec->strategy == PARTITION_STRATEGY_LIST);
Assert(boundinfo && boundinfo->strategy == PARTITION_STRATEGY_LIST);
/*
* Search each value of the new partition "spec" in the existing
* partitions. All of them should be in the split partition (with Oid
* splitPartOid).
*/
foreach_node(Const, val, spec->listdatums)
{
overlap_location = exprLocation((Node *) val);
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 \"%s\" does not have",
relname,
get_rel_name(splitPartOid)),
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 \"%s\" does not have",
relname,
get_rel_name(splitPartOid)),
parser_errposition(pstate, overlap_location));
}
if (overlap)
{
Assert(with >= 0);
ereport(ERROR,
errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("new partition \"%s\" would overlap with another (not split) partition \"%s\"",
relname, get_rel_name(partdesc->oids[with])),
parser_errposition(pstate, overlap_location));
}
}
/*
* find_value_in_new_partitions_list
*
* (function for BY LIST partitioning)
*
* Function returns true iff any of the new partitions contains the value
* "value".
*
* partsupfunc: information about the comparison function associated with
* the partition key
* partcollation: partitioning collation
* parts: pointer to an array with new partition 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)
{
for (int i = 0; i < nparts; i++)
{
SinglePartitionSpec *sps = parts[i];
foreach_node(Const, val, sps->bound->listdatums)
{
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) are contained
* in new partitions.
*
* parent: partitioned table
* partOid: split partition Oid
* parts: pointer to an array with new partition descriptions
* nparts: number of new partitions
* pstate: pointer to ParseState struct to 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;
Datum datum = PointerGetDatum(NULL);
Assert(key->strategy == PARTITION_STRATEGY_LIST);
/*
* Special processing for NULL value. Search for a NULL value if the split
* partition (partOid) contains it.
*/
if (partition_bound_accepts_nulls(boundinfo) &&
partdesc->oids[boundinfo->null_index] == partOid)
{
if (!find_value_in_new_partitions_list(&key->partsupfunc[0],
key->partcollation, parts, nparts, datum, true))
found = false;
}
if (!found)
ereport(ERROR,
errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("new partitions combined partition bounds do not contain value (%s) but split partition \"%s\" does",
"NULL",
get_rel_name(partOid)),
errhint("%s require combined bounds of new partitions must exactly match the bound of the split partition",
"ALTER TABLE ... SPLIT PARTITION"));
/*
* Search all values of split partition with partOid in the PartitionDesc
* of partitioned table.
*/
for (i = 0; i < boundinfo->ndatums; i++)
{
if (partdesc->oids[boundinfo->indexes[i]] == partOid)
{
/* We found the value that the 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;
/*
* Make a Const for getting the string representation of the missing
* 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 combined partition bounds do not contain value (%s) but split partition \"%s\" does",
deparse_expression((Node *) notFoundVal, NIL, false, false),
get_rel_name(partOid)),
errhint("%s require combined bounds of new partitions must exactly match the bound of the split partition",
"ALTER TABLE ... SPLIT PARTITION"));
}
}
/*
* check_partitions_for_split
*
* Checks new partitions for the SPLIT PARTITION command:
* 1. Bounds of new partitions should not overlap with new and existing
* partitions.
* 2. In the case when new or existing partitions contain the DEFAULT
* partition, new partitions can have any bounds inside the split partition
* bound (can be spaces between partition bounds).
* 3. In case new partitions don't contain the DEFAULT partition and the
* partitioned table does not have the DEFAULT partition, the following
* should be true: the sum of the bounds of new partitions should be equal
& to the bound of the split partition.
*
* parent: partitioned table
* splitPartOid: split partition Oid
* partlist: list of new partitions after partition split
* pstate: pointer to ParseState struct for determine error position
*/
void
check_partitions_for_split(Relation parent,
Oid splitPartOid,
List *partlist,
ParseState *pstate)
{
PartitionKey key;
char strategy;
Oid defaultPartOid;
bool isSplitPartDefault;
bool createDefaultPart = false;
int default_index = -1;
int i;
SinglePartitionSpec **new_parts;
SinglePartitionSpec *spsPrev = NULL;
/*
* nparts counts the number of split partitions, but it exclude the
* default partition.
*/
int nparts = 0;
key = RelationGetPartitionKey(parent);
strategy = get_partition_strategy(key);
defaultPartOid =
get_default_oid_from_partdesc(RelationGetPartitionDesc(parent, true));
Assert(strategy == PARTITION_STRATEGY_RANGE ||
strategy == PARTITION_STRATEGY_LIST);
/*
* Make an array new_parts with new partitions except the DEFAULT
* partition.
*/
new_parts = (SinglePartitionSpec **)
palloc0(list_length(partlist) * sizeof(SinglePartitionSpec *));
/* isSplitPartDefault flag: is split partition a DEFAULT partition? */
isSplitPartDefault = (defaultPartOid == splitPartOid);
foreach_node(SinglePartitionSpec, sps, partlist)
{
if (sps->bound->is_default)
default_index = foreach_current_index(sps);
else
new_parts[nparts++] = sps;
}
/* An indicator that the DEFAULT partition will be created. */
if (default_index != -1)
{
createDefaultPart = true;
Assert(nparts == list_length(partlist) - 1);
}
if (strategy == PARTITION_STRATEGY_RANGE)
{
PartitionRangeBound **lower_bounds;
SinglePartitionSpec **tmp_new_parts;
/*
* To simplify the check for ranges of new partitions, we need to sort
* all partitions in ascending order of their bounds (we compare the
* lower bound only).
*/
lower_bounds = (PartitionRangeBound **)
palloc0(nparts * sizeof(PartitionRangeBound *));
/* Create an 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 the array of lower bounds. */
qsort_arg(lower_bounds, nparts, sizeof(PartitionRangeBound *),
qsort_partition_rbound_cmp, (void *) key);
/* Reorder the 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);
}
for (i = 0; i < nparts; i++)
{
SinglePartitionSpec *sps = new_parts[i];
if (isSplitPartDefault)
{
/*
* When the split partition is the 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 the bounds of the current partition are inside the
* bounds of the split partition. For range partitioning: checks
* that the upper bound of the previous partition is equal to the
* lower bound of the current partition. For list partitioning:
* checks that the split partition contains all values of the
* 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, first, last,
createDefaultPart, 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,
createDefaultPart,
false,
pstate);
spsPrev = sps;
}
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 the split partition are contained
* in the new partitions. Skip this check if the DEFAULT partition
* exists.
*/
if (!createDefaultPart)
check_parent_values_in_new_partitions(parent, splitPartOid,
new_parts, nparts, pstate);
}
pfree(new_parts);
}

@ -2773,7 +2773,7 @@ match_previous_words(int pattern_id,
"OWNER TO", "SET", "VALIDATE CONSTRAINT", "OWNER TO", "SET", "VALIDATE CONSTRAINT",
"REPLICA IDENTITY", "ATTACH PARTITION", "REPLICA IDENTITY", "ATTACH PARTITION",
"DETACH PARTITION", "FORCE ROW LEVEL SECURITY", "DETACH PARTITION", "FORCE ROW LEVEL SECURITY",
"MERGE PARTITIONS (", "SPLIT PARTITION", "MERGE PARTITIONS (",
"OF", "NOT OF"); "OF", "NOT OF");
/* ALTER TABLE xxx ADD */ /* ALTER TABLE xxx ADD */
else if (Matches("ALTER", "TABLE", MatchAny, "ADD")) else if (Matches("ALTER", "TABLE", MatchAny, "ADD"))
@ -3036,10 +3036,10 @@ match_previous_words(int pattern_id,
COMPLETE_WITH("FROM (", "IN (", "WITH ("); COMPLETE_WITH("FROM (", "IN (", "WITH (");
/* /*
* If we have ALTER TABLE <foo> DETACH PARTITION, provide a list of * If we have ALTER TABLE <foo> DETACH|SPLIT PARTITION, provide a list of
* partitions of <foo>. * partitions of <foo>.
*/ */
else if (Matches("ALTER", "TABLE", MatchAny, "DETACH", "PARTITION")) else if (Matches("ALTER", "TABLE", MatchAny, "DETACH|SPLIT", "PARTITION"))
{ {
set_completion_reference(prev3_wd); set_completion_reference(prev3_wd);
COMPLETE_WITH_SCHEMA_QUERY(Query_for_partition_of_table); COMPLETE_WITH_SCHEMA_QUERY(Query_for_partition_of_table);
@ -3047,6 +3047,10 @@ match_previous_words(int pattern_id,
else if (Matches("ALTER", "TABLE", MatchAny, "DETACH", "PARTITION", MatchAny)) else if (Matches("ALTER", "TABLE", MatchAny, "DETACH", "PARTITION", MatchAny))
COMPLETE_WITH("CONCURRENTLY", "FINALIZE"); COMPLETE_WITH("CONCURRENTLY", "FINALIZE");
/* ALTER TABLE <name> SPLIT PARTITION <name> */
else if (Matches("ALTER", "TABLE", MatchAny, "SPLIT", "PARTITION", MatchAny))
COMPLETE_WITH("INTO ( PARTITION");
/* ALTER TABLE <name> MERGE PARTITIONS ( */ /* ALTER TABLE <name> MERGE PARTITIONS ( */
else if (Matches("ALTER", "TABLE", MatchAny, "MERGE", "PARTITIONS", "(")) else if (Matches("ALTER", "TABLE", MatchAny, "MERGE", "PARTITIONS", "("))
{ {

@ -965,17 +965,40 @@ typedef struct PartitionRangeDatum
ParseLoc location; /* token location, or -1 if unknown */ ParseLoc location; /* token location, or -1 if unknown */
} PartitionRangeDatum; } PartitionRangeDatum;
/*
* PartitionDesc - info about a single partition for the ALTER TABLE SPLIT
* PARTITION command
*/
typedef struct SinglePartitionSpec
{
NodeTag type;
RangeVar *name; /* name of partition */
PartitionBoundSpec *bound; /* FOR VALUES, if attaching */
} SinglePartitionSpec;
/* /*
* PartitionCmd - info for ALTER TABLE/INDEX ATTACH/DETACH PARTITION and for * PartitionCmd - info for ALTER TABLE/INDEX ATTACH/DETACH PARTITION and for
* ALTER TABLE MERGE PARTITIONS commands * ALTER TABLE SPLIT/MERGE PARTITION(S) commands
*/ */
typedef struct PartitionCmd typedef struct PartitionCmd
{ {
NodeTag type; NodeTag type;
RangeVar *name; /* name of partition to attach/detach/merge */
PartitionBoundSpec *bound; /* FOR VALUES, if attaching */ /* name of partition to attach/detach/merge/split */
List *partlist; /* list of partitions to be merged, used in RangeVar *name;
* ALTER TABLE MERGE PARTITIONS */
/* FOR VALUES, if attaching */
PartitionBoundSpec *bound;
/*
* list of partitions to be split/merged, used in ALTER TABLE MERGE
* PARTITIONS and ALTER TABLE SPLIT PARTITIONS. For merge partitions,
* partlist is a list of RangeVar; For split partition, it is a list of
* SinglePartitionSpec.
*/
List *partlist;
bool concurrent; bool concurrent;
} PartitionCmd; } PartitionCmd;
@ -2479,6 +2502,7 @@ typedef enum AlterTableType
AT_AttachPartition, /* ATTACH PARTITION */ AT_AttachPartition, /* ATTACH PARTITION */
AT_DetachPartition, /* DETACH PARTITION */ AT_DetachPartition, /* DETACH PARTITION */
AT_DetachPartitionFinalize, /* DETACH PARTITION FINALIZE */ AT_DetachPartitionFinalize, /* DETACH PARTITION FINALIZE */
AT_SplitPartition, /* SPLIT PARTITION */
AT_MergePartitions, /* MERGE PARTITIONS */ AT_MergePartitions, /* MERGE PARTITIONS */
AT_AddIdentity, /* ADD IDENTITY */ AT_AddIdentity, /* ADD IDENTITY */
AT_SetIdentity, /* SET identity column options */ AT_SetIdentity, /* SET identity column options */

@ -424,6 +424,7 @@ PG_KEYWORD("smallint", SMALLINT, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("snapshot", SNAPSHOT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("snapshot", SNAPSHOT, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("some", SOME, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("some", SOME, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("source", SOURCE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("source", SOURCE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("split", SPLIT, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("sql", SQL_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("sql", SQL_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("stable", STABLE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("stable", STABLE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("standalone", STANDALONE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("standalone", STANDALONE_P, UNRESERVED_KEYWORD, BARE_LABEL)

@ -143,6 +143,10 @@ extern int partition_range_datum_bsearch(FmgrInfo *partsupfunc,
extern int partition_hash_bsearch(PartitionBoundInfo boundinfo, extern int partition_hash_bsearch(PartitionBoundInfo boundinfo,
int modulus, int remainder); 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, extern void calculate_partition_bound_for_merge(Relation parent,
List *partNames, List *partNames,
List *partOids, List *partOids,

@ -0,0 +1,230 @@
Parsed test spec with 2 sessions
starting permutation: s1b s1splt s2b s2i s1c s2c s2s
step s1b: BEGIN;
step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO
(PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15),
PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20));
step s2b: BEGIN;
step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
step s1c: COMMIT;
step s2i: <... completed>
step s2c: COMMIT;
step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i;
tableoid | i|t
-------------+--+------
tpart_00_10 | 1|text01
tpart_00_10 | 5|text05
tpart_15_20 |15|text15
tpart_20_30 |25|text25
tpart_default|35|text35
(5 rows)
starting permutation: s1b s1splt s2brr s2i s1c s2c s2s
step s1b: BEGIN;
step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO
(PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15),
PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20));
step s2brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
step s1c: COMMIT;
step s2i: <... completed>
step s2c: COMMIT;
step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i;
tableoid | i|t
-------------+--+------
tpart_00_10 | 1|text01
tpart_00_10 | 5|text05
tpart_15_20 |15|text15
tpart_20_30 |25|text25
tpart_default|35|text35
(5 rows)
starting permutation: s1b s1splt s2bs s2i s1c s2c s2s
step s1b: BEGIN;
step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO
(PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15),
PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20));
step s2bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
step s1c: COMMIT;
step s2i: <... completed>
step s2c: COMMIT;
step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i;
tableoid | i|t
-------------+--+------
tpart_00_10 | 1|text01
tpart_00_10 | 5|text05
tpart_15_20 |15|text15
tpart_20_30 |25|text25
tpart_default|35|text35
(5 rows)
starting permutation: s1brr s1splt s2b s2i s1c s2c s2s
step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO
(PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15),
PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20));
step s2b: BEGIN;
step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
step s1c: COMMIT;
step s2i: <... completed>
step s2c: COMMIT;
step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i;
tableoid | i|t
-------------+--+------
tpart_00_10 | 1|text01
tpart_00_10 | 5|text05
tpart_15_20 |15|text15
tpart_20_30 |25|text25
tpart_default|35|text35
(5 rows)
starting permutation: s1brr s1splt s2brr s2i s1c s2c s2s
step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO
(PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15),
PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20));
step s2brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
step s1c: COMMIT;
step s2i: <... completed>
step s2c: COMMIT;
step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i;
tableoid | i|t
-------------+--+------
tpart_00_10 | 1|text01
tpart_00_10 | 5|text05
tpart_15_20 |15|text15
tpart_20_30 |25|text25
tpart_default|35|text35
(5 rows)
starting permutation: s1brr s1splt s2bs s2i s1c s2c s2s
step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO
(PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15),
PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20));
step s2bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
step s1c: COMMIT;
step s2i: <... completed>
step s2c: COMMIT;
step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i;
tableoid | i|t
-------------+--+------
tpart_00_10 | 1|text01
tpart_00_10 | 5|text05
tpart_15_20 |15|text15
tpart_20_30 |25|text25
tpart_default|35|text35
(5 rows)
starting permutation: s1bs s1splt s2b s2i s1c s2c s2s
step s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO
(PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15),
PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20));
step s2b: BEGIN;
step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
step s1c: COMMIT;
step s2i: <... completed>
step s2c: COMMIT;
step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i;
tableoid | i|t
-------------+--+------
tpart_00_10 | 1|text01
tpart_00_10 | 5|text05
tpart_15_20 |15|text15
tpart_20_30 |25|text25
tpart_default|35|text35
(5 rows)
starting permutation: s1bs s1splt s2brr s2i s1c s2c s2s
step s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO
(PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15),
PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20));
step s2brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
step s1c: COMMIT;
step s2i: <... completed>
step s2c: COMMIT;
step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i;
tableoid | i|t
-------------+--+------
tpart_00_10 | 1|text01
tpart_00_10 | 5|text05
tpart_15_20 |15|text15
tpart_20_30 |25|text25
tpart_default|35|text35
(5 rows)
starting permutation: s1bs s1splt s2bs s2i s1c s2c s2s
step s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO
(PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15),
PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20));
step s2bs: BEGIN ISOLATION LEVEL SERIALIZABLE;
step s2i: INSERT INTO tpart VALUES (1, 'text01'); <waiting ...>
step s1c: COMMIT;
step s2i: <... completed>
step s2c: COMMIT;
step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i;
tableoid | i|t
-------------+--+------
tpart_00_10 | 1|text01
tpart_00_10 | 5|text05
tpart_15_20 |15|text15
tpart_20_30 |25|text25
tpart_default|35|text35
(5 rows)
starting permutation: s1b s1splt s2b s2u 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 s2u: UPDATE tpart SET i = 16 where i = 5; <waiting ...>
step s1c: COMMIT;
step s2u: <... completed>
step s2c: COMMIT;
step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i;
tableoid | i|t
-------------+--+------
tpart_15_20 |15|text15
tpart_15_20 |16|text05
tpart_20_30 |25|text25
tpart_default|35|text35
(4 rows)
starting permutation: s1b s1splt s2b s2u2 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 s2u2: UPDATE tpart SET i = 11 where i = 15; <waiting ...>
step s1c: COMMIT;
step s2u2: <... completed>
step s2c: COMMIT;
step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i;
tableoid | i|t
-------------+--+------
tpart_00_10 | 5|text05
tpart_10_15 |11|text15
tpart_20_30 |25|text25
tpart_default|35|text35
(4 rows)

@ -110,6 +110,7 @@ test: partition-key-update-2
test: partition-key-update-3 test: partition-key-update-3
test: partition-key-update-4 test: partition-key-update-4
test: partition-merge test: partition-merge
test: partition-split
test: plpgsql-toast test: plpgsql-toast
test: cluster-conflict test: cluster-conflict
test: cluster-conflict-partition test: cluster-conflict-partition

@ -0,0 +1,62 @@
# Verify that SPLIT operation locks DML operations with partitioned table
setup
{
DROP TABLE IF EXISTS tpart;
CREATE TABLE tpart(i int, t text) partition by range(i);
CREATE TABLE tpart_00_10 PARTITION OF tpart FOR VALUES FROM (0) TO (10);
CREATE TABLE tpart_10_20 PARTITION OF tpart FOR VALUES FROM (10) TO (20);
CREATE TABLE tpart_20_30 PARTITION OF tpart FOR VALUES FROM (20) TO (30);
CREATE TABLE tpart_default PARTITION OF tpart DEFAULT;
INSERT INTO tpart VALUES (5, 'text05');
INSERT INTO tpart VALUES (15, 'text15');
INSERT INTO tpart VALUES (25, 'text25');
INSERT INTO tpart VALUES (35, 'text35');
}
teardown
{
DROP TABLE tpart;
}
session s1
step s1b { BEGIN; }
step s1brr { BEGIN ISOLATION LEVEL REPEATABLE READ; }
step s1bs { BEGIN ISOLATION LEVEL SERIALIZABLE; }
step s1splt { ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO
(PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15),
PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20)); }
step s1c { COMMIT; }
session s2
step s2b { BEGIN; }
step s2brr { BEGIN ISOLATION LEVEL REPEATABLE READ; }
step s2bs { BEGIN ISOLATION LEVEL SERIALIZABLE; }
step s2i { INSERT INTO tpart VALUES (1, 'text01'); }
step s2c { COMMIT; }
step s2s { SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; }
step s2u { UPDATE tpart SET i = 16 where i = 5; }
step s2u2 { UPDATE tpart SET i = 11 where i = 15; }
# s1 starts SPLIT PARTITION then s2 trying to insert row and
# waits until s1 finished SPLIT operation.
permutation s1b s1splt s2b s2i s1c s2c s2s
permutation s1b s1splt s2brr s2i s1c s2c s2s
permutation s1b s1splt s2bs s2i s1c s2c s2s
permutation s1brr s1splt s2b s2i s1c s2c s2s
permutation s1brr s1splt s2brr s2i s1c s2c s2s
permutation s1brr s1splt s2bs s2i s1c s2c s2s
permutation s1bs s1splt s2b s2i s1c s2c s2s
permutation s1bs s1splt s2brr s2i s1c s2c s2s
permutation s1bs s1splt s2bs s2i s1c s2c s2s
# Tuple routing between partitions.
permutation s1b s1splt s2b s2u s1c s2c s2s
# Tuple routing inside splitting partition.
permutation s1b s1splt s2b s2u2 s1c s2c s2s

@ -118,6 +118,11 @@ NOTICE: DDL test: type simple, tag CREATE TABLE
ALTER TABLE part MERGE PARTITIONS (part1, part2) INTO part1; ALTER TABLE part MERGE PARTITIONS (part1, part2) INTO part1;
NOTICE: DDL test: type alter table, tag ALTER TABLE NOTICE: DDL test: type alter table, tag ALTER TABLE
NOTICE: subcommand: type MERGE PARTITIONS desc <NULL> NOTICE: subcommand: type MERGE PARTITIONS desc <NULL>
ALTER TABLE part SPLIT PARTITION part1 INTO
(PARTITION part1 FOR VALUES FROM (1) to (100),
PARTITION part2 FOR VALUES FROM (100) to (200));
NOTICE: DDL test: type alter table, tag ALTER TABLE
NOTICE: subcommand: type SPLIT PARTITION desc <NULL>
ALTER TABLE part ADD PRIMARY KEY (a); ALTER TABLE part ADD PRIMARY KEY (a);
NOTICE: DDL test: type alter table, tag ALTER TABLE NOTICE: DDL test: type alter table, tag ALTER TABLE
NOTICE: subcommand: type ADD CONSTRAINT (and recurse) desc constraint part_a_not_null on table part NOTICE: subcommand: type ADD CONSTRAINT (and recurse) desc constraint part_a_not_null on table part

@ -63,6 +63,10 @@ DROP TABLE part2;
CREATE TABLE part2 PARTITION OF part FOR VALUES FROM (100) to (200); CREATE TABLE part2 PARTITION OF part FOR VALUES FROM (100) to (200);
ALTER TABLE part MERGE PARTITIONS (part1, part2) INTO part1; ALTER TABLE part MERGE PARTITIONS (part1, part2) INTO part1;
ALTER TABLE part SPLIT PARTITION part1 INTO
(PARTITION part1 FOR VALUES FROM (1) to (100),
PARTITION part2 FOR VALUES FROM (100) to (200));
ALTER TABLE part ADD PRIMARY KEY (a); ALTER TABLE part ADD PRIMARY KEY (a);
CREATE TABLE tbl ( CREATE TABLE tbl (

@ -296,6 +296,9 @@ get_altertable_subcmdinfo(PG_FUNCTION_ARGS)
case AT_DetachPartitionFinalize: case AT_DetachPartitionFinalize:
strtype = "DETACH PARTITION ... FINALIZE"; strtype = "DETACH PARTITION ... FINALIZE";
break; break;
case AT_SplitPartition:
strtype = "SPLIT PARTITION";
break;
case AT_MergePartitions: case AT_MergePartitions:
strtype = "MERGE PARTITIONS"; strtype = "MERGE PARTITIONS";
break; break;

File diff suppressed because it is too large Load Diff

@ -123,7 +123,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 # The stats test resets stats, so nothing else needing stats access can be in
# this group. # this group.
# ---------- # ----------
test: partition_merge partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_lz4 memoize stats predicate numa eager_aggregate test: partition_merge partition_split partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_lz4 memoize stats predicate numa eager_aggregate
# event_trigger depends on create_am and cannot run concurrently with # event_trigger depends on create_am and cannot run concurrently with
# any test that runs DDL # any test that runs DDL

File diff suppressed because it is too large Load Diff

@ -2800,6 +2800,7 @@ SimpleStats
SimpleStringList SimpleStringList
SimpleStringListCell SimpleStringListCell
SingleBoundSortItem SingleBoundSortItem
SinglePartitionSpec
Size Size
SkipPages SkipPages
SkipSupport SkipSupport
@ -2868,6 +2869,7 @@ SpecialJoinInfo
SpinDelayStatus SpinDelayStatus
SplitInterval SplitInterval
SplitLR SplitLR
SplitPartitionContext
SplitPageLayout SplitPageLayout
SplitPoint SplitPoint
SplitTextOutputData SplitTextOutputData

Loading…
Cancel
Save