Commit 71f4c8c6 authored by Alvaro Herrera's avatar Alvaro Herrera

ALTER TABLE ... DETACH PARTITION ... CONCURRENTLY

Allow a partition be detached from its partitioned table without
blocking concurrent queries, by running in two transactions and only
requiring ShareUpdateExclusive in the partitioned table.

Because it runs in two transactions, it cannot be used in a transaction
block.  This is the main reason to use dedicated syntax: so that users
can choose to use the original mode if they need it.  But also, it
doesn't work when a default partition exists (because an exclusive lock
would still need to be obtained on it, in order to change its partition
constraint.)

In case the second transaction is cancelled or a crash occurs, there's
ALTER TABLE .. DETACH PARTITION .. FINALIZE, which executes the final
steps.

The main trick to make this work is the addition of column
pg_inherits.inhdetachpending, initially false; can only be set true in
the first part of this command.  Once that is committed, concurrent
transactions that use a PartitionDirectory will include or ignore
partitions so marked: in optimizer they are ignored if the row is marked
committed for the snapshot; in executor they are always included.  As a
result, and because of the way PartitionDirectory caches partition
descriptors, queries that were planned before the detach will see the
rows in the detached partition and queries that are planned after the
detach, won't.

A CHECK constraint is created that duplicates the partition constraint.
This is probably not strictly necessary, and some users will prefer to
remove it afterwards, but if the partition is re-attached to a
partitioned table, the constraint needn't be rechecked.

Author: Álvaro Herrera <alvherre@alvh.no-ip.org>
Reviewed-by: default avatarAmit Langote <amitlangote09@gmail.com>
Reviewed-by: default avatarJustin Pryzby <pryzby@telsasoft.com>
Discussion: https://postgr.es/m/20200803234854.GA24158@alvherre.pgsql
parent 650d6235
......@@ -4497,6 +4497,16 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
when using declarative partitioning.
</para></entry>
</row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>inhdetachpending</structfield> <type>bool</type>
</para>
<para>
<literal>true</literal> for a partition that is in the process of
being detached; <literal>false</literal> otherwise.
</para></entry>
</row>
</tbody>
</tgroup>
</table>
......
......@@ -36,7 +36,7 @@ ALTER TABLE ALL IN TABLESPACE <replaceable class="parameter">name</replaceable>
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>
DETACH PARTITION <replaceable class="parameter">partition_name</replaceable> [ CONCURRENTLY | FINALIZE ]
<phrase>where <replaceable class="parameter">action</replaceable> is one of:</phrase>
......@@ -954,7 +954,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry>
<term><literal>DETACH PARTITION</literal> <replaceable class="parameter">partition_name</replaceable></term>
<term><literal>DETACH PARTITION <replaceable class="parameter">partition_name</replaceable> [ CONCURRENTLY | FINALIZE ]</literal></term>
<listitem>
<para>
This form detaches the specified partition of the target table. The detached
......@@ -965,6 +966,27 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<literal>SHARE</literal> lock is obtained on any tables that reference
this partitioned table in foreign key constraints.
</para>
<para>
If <literal>CONCURRENTLY</literal> is specified, it runs using a reduced
lock level to avoid blocking other sessions that might be accessing the
partitioned table. In this mode, two transactions are used internally.
During the first transaction, a <literal>SHARE UPDATE EXCLUSIVE</literal>
lock is taken on both parent table and partition, and the partition is
marked as undergoing detach; at that point, the transaction is committed
and all other transactions using the partitioned table are waited for.
Once all those transactions have completed, the second transaction
acquires <literal>SHARE UPDATE EXCLUSIVE</literal> on the partitioned
table and <literal>ACCESS EXCLUSIVE</literal> on the partition,
and the detach process completes. A <literal>CHECK</literal> constraint
that duplicates the partition constraint is added to the partition.
<literal>CONCURRENTLY</literal> cannot be run in a transaction block and
is not allowed if the partitioned table contains a default partition.
</para>
<para>
If <literal>FINALIZE</literal> is specified, a previous
<literal>DETACH CONCURRENTLY</literal> invocation that was cancelled or
interrupted is completed.
</para>
</listitem>
</varlistentry>
......
......@@ -1923,7 +1923,12 @@ heap_drop_with_catalog(Oid relid)
elog(ERROR, "cache lookup failed for relation %u", relid);
if (((Form_pg_class) GETSTRUCT(tuple))->relispartition)
{
parentOid = get_partition_parent(relid);
/*
* We have to lock the parent if the partition is being detached,
* because it's possible that some query still has a partition
* descriptor that includes this partition.
*/
parentOid = get_partition_parent(relid, true);
LockRelationOid(parentOid, AccessExclusiveLock);
/*
......@@ -2559,10 +2564,12 @@ StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal)
* Returns a list of CookedConstraint nodes that shows the cooked form of
* the default and constraint expressions added to the relation.
*
* NB: caller should have opened rel with AccessExclusiveLock, and should
* hold that lock till end of transaction. Also, we assume the caller has
* done a CommandCounterIncrement if necessary to make the relation's catalog
* tuples visible.
* NB: caller should have opened rel with some self-conflicting lock mode,
* and should hold that lock till end of transaction; for normal cases that'll
* be AccessExclusiveLock, but if caller knows that the constraint is already
* enforced by some other means, it can be ShareUpdateExclusiveLock. Also, we
* assume the caller has done a CommandCounterIncrement if necessary to make
* the relation's catalog tuples visible.
*/
List *
AddRelationNewConstraints(Relation rel,
......@@ -3831,7 +3838,8 @@ StorePartitionBound(Relation rel, Relation parent, PartitionBoundSpec *bound)
* relcache entry for that partition every time a partition is added or
* removed.
*/
defaultPartOid = get_default_oid_from_partdesc(RelationGetPartitionDesc(parent));
defaultPartOid =
get_default_oid_from_partdesc(RelationGetPartitionDesc(parent, false));
if (OidIsValid(defaultPartOid))
CacheInvalidateRelcacheByRelid(defaultPartOid);
......
......@@ -1837,7 +1837,7 @@ index_concurrently_swap(Oid newIndexId, Oid oldIndexId, const char *oldName)
List *ancestors = get_partition_ancestors(oldIndexId);
Oid parentIndexRelid = linitial_oid(ancestors);
DeleteInheritsTuple(oldIndexId, parentIndexRelid);
DeleteInheritsTuple(oldIndexId, parentIndexRelid, false, NULL);
StoreSingleInheritance(newIndexId, parentIndexRelid, 1);
list_free(ancestors);
......@@ -2487,7 +2487,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
/*
* fix INHERITS relation
*/
DeleteInheritsTuple(indexId, InvalidOid);
DeleteInheritsTuple(indexId, InvalidOid, false, NULL);
/*
* We are presently too lazy to attempt to compute the new correct value
......
......@@ -32,7 +32,8 @@
#include "utils/rel.h"
#include "utils/syscache.h"
static Oid get_partition_parent_worker(Relation inhRel, Oid relid);
static Oid get_partition_parent_worker(Relation inhRel, Oid relid,
bool *detach_pending);
static void get_partition_ancestors_worker(Relation inhRel, Oid relid,
List **ancestors);
......@@ -42,23 +43,32 @@ static void get_partition_ancestors_worker(Relation inhRel, Oid relid,
*
* Returns inheritance parent of a partition by scanning pg_inherits
*
* If the partition is in the process of being detached, an error is thrown,
* unless even_if_detached is passed as true.
*
* Note: Because this function assumes that the relation whose OID is passed
* as an argument will have precisely one parent, it should only be called
* when it is known that the relation is a partition.
*/
Oid
get_partition_parent(Oid relid)
get_partition_parent(Oid relid, bool even_if_detached)
{
Relation catalogRelation;
Oid result;
bool detach_pending;
catalogRelation = table_open(InheritsRelationId, AccessShareLock);
result = get_partition_parent_worker(catalogRelation, relid);
result = get_partition_parent_worker(catalogRelation, relid,
&detach_pending);
if (!OidIsValid(result))
elog(ERROR, "could not find tuple for parent of relation %u", relid);
if (detach_pending && !even_if_detached)
elog(ERROR, "relation %u has no parent because it's being detached",
relid);
table_close(catalogRelation, AccessShareLock);
return result;
......@@ -68,15 +78,20 @@ get_partition_parent(Oid relid)
* get_partition_parent_worker
* Scan the pg_inherits relation to return the OID of the parent of the
* given relation
*
* If the partition is being detached, *detach_pending is set true (but the
* original parent is still returned.)
*/
static Oid
get_partition_parent_worker(Relation inhRel, Oid relid)
get_partition_parent_worker(Relation inhRel, Oid relid, bool *detach_pending)
{
SysScanDesc scan;
ScanKeyData key[2];
Oid result = InvalidOid;
HeapTuple tuple;
*detach_pending = false;
ScanKeyInit(&key[0],
Anum_pg_inherits_inhrelid,
BTEqualStrategyNumber, F_OIDEQ,
......@@ -93,6 +108,9 @@ get_partition_parent_worker(Relation inhRel, Oid relid)
{
Form_pg_inherits form = (Form_pg_inherits) GETSTRUCT(tuple);
/* Let caller know of partition being detached */
if (form->inhdetachpending)
*detach_pending = true;
result = form->inhparent;
}
......@@ -134,10 +152,14 @@ static void
get_partition_ancestors_worker(Relation inhRel, Oid relid, List **ancestors)
{
Oid parentOid;
bool detach_pending;
/* Recursion ends at the topmost level, ie., when there's no parent */
parentOid = get_partition_parent_worker(inhRel, relid);
if (parentOid == InvalidOid)
/*
* Recursion ends at the topmost level, ie., when there's no parent; also
* when the partition is being detached.
*/
parentOid = get_partition_parent_worker(inhRel, relid, &detach_pending);
if (parentOid == InvalidOid || detach_pending)
return;
*ancestors = lappend_oid(*ancestors, parentOid);
......@@ -170,7 +192,7 @@ index_get_partition(Relation partition, Oid indexId)
ReleaseSysCache(tup);
if (!ispartition)
continue;
if (get_partition_parent(partIdx) == indexId)
if (get_partition_parent(partIdx, false) == indexId)
{
list_free(idxlist);
return partIdx;
......
......@@ -29,6 +29,7 @@
#include "utils/builtins.h"
#include "utils/fmgroids.h"
#include "utils/memutils.h"
#include "utils/snapmgr.h"
#include "utils/syscache.h"
/*
......@@ -50,9 +51,14 @@ typedef struct SeenRelsEntry
* given rel; caller should already have locked it). If lockmode is NoLock
* then no locks are acquired, but caller must beware of race conditions
* against possible DROPs of child relations.
*
* include_detached says to include all partitions, even if they're marked
* detached. Passing it as false means they might or might not be included,
* depending on the visibility of the pg_inherits row for the active snapshot.
*/
List *
find_inheritance_children(Oid parentrelId, LOCKMODE lockmode)
find_inheritance_children(Oid parentrelId, bool include_detached,
LOCKMODE lockmode)
{
List *list = NIL;
Relation relation;
......@@ -91,6 +97,30 @@ find_inheritance_children(Oid parentrelId, LOCKMODE lockmode)
while ((inheritsTuple = systable_getnext(scan)) != NULL)
{
/*
* Cope with partitions concurrently being detached. When we see a
* partition marked "detach pending", we only include it in the set of
* visible partitions if caller requested all detached partitions, or
* if its pg_inherits tuple's xmin is still visible to the active
* snapshot.
*
* The reason for this check is that we want to avoid seeing the
* partition as alive in RI queries during REPEATABLE READ or
* SERIALIZABLE transactions.
*/
if (((Form_pg_inherits) GETSTRUCT(inheritsTuple))->inhdetachpending &&
!include_detached)
{
TransactionId xmin;
Snapshot snap;
xmin = HeapTupleHeaderGetXmin(inheritsTuple->t_data);
snap = GetActiveSnapshot();
if (!XidInMVCCSnapshot(xmin, snap))
continue;
}
inhrelid = ((Form_pg_inherits) GETSTRUCT(inheritsTuple))->inhrelid;
if (numoids >= maxoids)
{
......@@ -160,6 +190,9 @@ find_inheritance_children(Oid parentrelId, LOCKMODE lockmode)
* given rel; caller should already have locked it). If lockmode is NoLock
* then no locks are acquired, but caller must beware of race conditions
* against possible DROPs of child relations.
*
* NB - No current callers of this routine are interested in children being
* concurrently detached, so there's no provision to include them.
*/
List *
find_all_inheritors(Oid parentrelId, LOCKMODE lockmode, List **numparents)
......@@ -199,7 +232,8 @@ find_all_inheritors(Oid parentrelId, LOCKMODE lockmode, List **numparents)
ListCell *lc;
/* Get the direct children of this rel */
currentchildren = find_inheritance_children(currentrel, lockmode);
currentchildren = find_inheritance_children(currentrel, false,
lockmode);
/*
* Add to the queue only those children not already seen. This avoids
......@@ -430,6 +464,7 @@ StoreSingleInheritance(Oid relationId, Oid parentOid, int32 seqNumber)
values[Anum_pg_inherits_inhrelid - 1] = ObjectIdGetDatum(relationId);
values[Anum_pg_inherits_inhparent - 1] = ObjectIdGetDatum(parentOid);
values[Anum_pg_inherits_inhseqno - 1] = Int32GetDatum(seqNumber);
values[Anum_pg_inherits_inhdetachpending - 1] = BoolGetDatum(false);
memset(nulls, 0, sizeof(nulls));
......@@ -449,10 +484,17 @@ StoreSingleInheritance(Oid relationId, Oid parentOid, int32 seqNumber)
* as InvalidOid, in which case all tuples matching inhrelid are deleted;
* otherwise only delete tuples with the specified inhparent.
*
* expect_detach_pending is the expected state of the inhdetachpending flag.
* If the catalog row does not match that state, an error is raised.
*
* childname is the partition name, if a table; pass NULL for regular
* inheritance or when working with other relation kinds.
*
* Returns whether at least one row was deleted.
*/
bool
DeleteInheritsTuple(Oid inhrelid, Oid inhparent)
DeleteInheritsTuple(Oid inhrelid, Oid inhparent, bool expect_detach_pending,
const char *childname)
{
bool found = false;
Relation catalogRelation;
......@@ -479,6 +521,29 @@ DeleteInheritsTuple(Oid inhrelid, Oid inhparent)
parent = ((Form_pg_inherits) GETSTRUCT(inheritsTuple))->inhparent;
if (!OidIsValid(inhparent) || parent == inhparent)
{
bool detach_pending;
detach_pending =
((Form_pg_inherits) GETSTRUCT(inheritsTuple))->inhdetachpending;
/*
* Raise error depending on state. This should only happen for
* partitions, but we have no way to cross-check.
*/
if (detach_pending && !expect_detach_pending)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot detach partition \"%s\"",
childname ? childname : "unknown relation"),
errdetail("The partition is being detached concurrently or has an unfinished detach."),
errhint("Use ALTER TABLE ... DETACH PARTITION ... FINALIZE to complete the pending detach operation")));
if (!detach_pending && expect_detach_pending)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot complete detaching partition \"%s\"",
childname ? childname : "unknown relation"),
errdetail("There's no pending concurrent detach.")));
CatalogTupleDelete(catalogRelation, &inheritsTuple->t_self);
found = true;
}
......@@ -490,3 +555,46 @@ DeleteInheritsTuple(Oid inhrelid, Oid inhparent)
return found;
}
/*
* Return whether the pg_inherits tuple for a partition has the "detach
* pending" flag set.
*/
bool
PartitionHasPendingDetach(Oid partoid)
{
Relation catalogRelation;
ScanKeyData key;
SysScanDesc scan;
HeapTuple inheritsTuple;
/* We don't have a good way to verify it is in fact a partition */
/*
* Find the pg_inherits entry by inhrelid. (There should only be one.)
*/
catalogRelation = table_open(InheritsRelationId, RowExclusiveLock);
ScanKeyInit(&key,
Anum_pg_inherits_inhrelid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(partoid));
scan = systable_beginscan(catalogRelation, InheritsRelidSeqnoIndexId,
true, NULL, 1, &key);
while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan)))
{
bool detached;
detached =
((Form_pg_inherits) GETSTRUCT(inheritsTuple))->inhdetachpending;
/* Done */
systable_endscan(scan);
table_close(catalogRelation, RowExclusiveLock);
return detached;
}
elog(ERROR, "relation %u is not a partition", partoid);
return false; /* keep compiler quiet */
}
......@@ -410,7 +410,7 @@ CompareOpclassOptions(Datum *opts1, Datum *opts2, int natts)
* GetCurrentVirtualXIDs. If, during any iteration, a particular vxid
* doesn't show up in the output, we know we can forget about it.
*/
static void
void
WaitForOlderSnapshots(TransactionId limitXmin, bool progress)
{
int n_old_snapshots;
......@@ -1123,7 +1123,7 @@ DefineIndex(Oid relationId,
*/
if (partitioned && stmt->relation && !stmt->relation->inh)
{
PartitionDesc pd = RelationGetPartitionDesc(rel);
PartitionDesc pd = RelationGetPartitionDesc(rel, false);
if (pd->nparts != 0)
flags |= INDEX_CREATE_INVALID;
......@@ -1180,7 +1180,7 @@ DefineIndex(Oid relationId,
*
* If we're called internally (no stmt->relation), recurse always.
*/
partdesc = RelationGetPartitionDesc(rel);
partdesc = RelationGetPartitionDesc(rel, false);
if ((!stmt->relation || stmt->relation->inh) && partdesc->nparts > 0)
{
int nparts = partdesc->nparts;
......
This diff is collapsed.
......@@ -1119,7 +1119,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
*/
if (partition_recurse)
{
PartitionDesc partdesc = RelationGetPartitionDesc(rel);
PartitionDesc partdesc = RelationGetPartitionDesc(rel, false);
List *idxs = NIL;
List *childTbls = NIL;
ListCell *l;
......@@ -1141,7 +1141,8 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
ListCell *l;
List *idxs = NIL;
idxs = find_inheritance_children(indexOid, ShareRowExclusiveLock);
idxs = find_inheritance_children(indexOid, false,
ShareRowExclusiveLock);
foreach(l, idxs)
childTbls = lappend_oid(childTbls,
IndexGetRelation(lfirst_oid(l),
......
......@@ -569,6 +569,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
int partidx)
{
ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
Oid partOid = dispatch->partdesc->oids[partidx];
Relation partrel;
int firstVarno = mtstate->resultRelInfo[0].ri_RangeTableIndex;
Relation firstResultRel = mtstate->resultRelInfo[0].ri_RelationDesc;
......@@ -579,7 +580,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
oldcxt = MemoryContextSwitchTo(proute->memcxt);
partrel = table_open(dispatch->partdesc->oids[partidx], RowExclusiveLock);
partrel = table_open(partOid, RowExclusiveLock);
leaf_part_rri = makeNode(ResultRelInfo);
InitResultRelInfo(leaf_part_rri,
......@@ -1065,9 +1066,21 @@ ExecInitPartitionDispatchInfo(EState *estate,
int dispatchidx;
MemoryContext oldcxt;
/*
* For data modification, it is better that executor does not include
* partitions being detached, except in snapshot-isolation mode. This
* means that a read-committed transaction immediately gets a "no
* partition for tuple" error when a tuple is inserted into a partition
* that's being detached concurrently, but a transaction in repeatable-
* read mode can still use the partition. Note that because partition
* detach uses ShareLock on the partition (which conflicts with DML),
* we're certain that the detach won't be able to complete until any
* inserting transaction is done.
*/
if (estate->es_partition_directory == NULL)
estate->es_partition_directory =
CreatePartitionDirectory(estate->es_query_cxt);
CreatePartitionDirectory(estate->es_query_cxt,
IsolationUsesXactSnapshot());
oldcxt = MemoryContextSwitchTo(proute->memcxt);
......@@ -1645,9 +1658,10 @@ ExecCreatePartitionPruneState(PlanState *planstate,
ListCell *lc;
int i;
/* Executor must always include detached partitions */
if (estate->es_partition_directory == NULL)
estate->es_partition_directory =
CreatePartitionDirectory(estate->es_query_cxt);
CreatePartitionDirectory(estate->es_query_cxt, true);
n_part_hierarchies = list_length(partitionpruneinfo->prune_infos);
Assert(n_part_hierarchies > 0);
......@@ -1713,9 +1727,12 @@ ExecCreatePartitionPruneState(PlanState *planstate,
partrel);
/*
* Initialize the subplan_map and subpart_map. Since detaching a
* partition requires AccessExclusiveLock, no partitions can have
* disappeared, nor can the bounds for any partition have changed.
* Initialize the subplan_map and subpart_map.
*
* Because we request detached partitions to be included, and
* detaching waits for old transactions, it is safe to assume that
* no partitions have disappeared since this query was planned.
*
* However, new partitions may have been added.
*/
Assert(partdesc->nparts >= pinfo->nparts);
......
......@@ -4737,6 +4737,7 @@ _copyPartitionCmd(const PartitionCmd *from)
COPY_NODE_FIELD(name);
COPY_NODE_FIELD(bound);
COPY_SCALAR_FIELD(concurrent);
return newnode;
}
......
......@@ -2975,6 +2975,7 @@ _equalPartitionCmd(const PartitionCmd *a, const PartitionCmd *b)
{
COMPARE_NODE_FIELD(name);
COMPARE_NODE_FIELD(bound);
COMPARE_SCALAR_FIELD(concurrent);
return true;
}
......
......@@ -2141,10 +2141,14 @@ set_relation_partition_info(PlannerInfo *root, RelOptInfo *rel,
{
PartitionDesc partdesc;
/* Create the PartitionDirectory infrastructure if we didn't already */
/*
* Create the PartitionDirectory infrastructure if we didn't already.
*/
if (root->glob->partition_directory == NULL)
{
root->glob->partition_directory =
CreatePartitionDirectory(CurrentMemoryContext);
CreatePartitionDirectory(CurrentMemoryContext, false);
}
partdesc = PartitionDirectoryLookup(root->glob->partition_directory,
relation);
......
......@@ -658,7 +658,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
EXTENSION EXTERNAL EXTRACT
FALSE_P FAMILY FETCH FILTER FIRST_P FLOAT_P FOLLOWING FOR
FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
FORCE FOREIGN FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS
GENERATED GLOBAL GRANT GRANTED GREATEST GROUP_P GROUPING GROUPS
......@@ -2108,12 +2108,13 @@ partition_cmd:
n->subtype = AT_AttachPartition;
cmd->name = $3;
cmd->bound = $4;
cmd->concurrent = false;
n->def = (Node *) cmd;
$$ = (Node *) n;
}
/* ALTER TABLE <name> DETACH PARTITION <partition_name> */
| DETACH PARTITION qualified_name
/* ALTER TABLE <name> DETACH PARTITION <partition_name> [CONCURRENTLY] */
| DETACH PARTITION qualified_name opt_concurrently
{
AlterTableCmd *n = makeNode(AlterTableCmd);
PartitionCmd *cmd = makeNode(PartitionCmd);
......@@ -2121,8 +2122,21 @@ partition_cmd:
n->subtype = AT_DetachPartition;
cmd->name = $3;
cmd->bound = NULL;
cmd->concurrent = $4;
n->def = (Node *) cmd;
$$ = (Node *) n;
}
| DETACH PARTITION qualified_name FINALIZE
{
AlterTableCmd *n = makeNode(AlterTableCmd);
PartitionCmd *cmd = makeNode(PartitionCmd);
n->subtype = AT_DetachPartitionFinalize;
cmd->name = $3;
cmd->bound = NULL;
cmd->concurrent = false;
n->def = (Node *) cmd;
$$ = (Node *) n;
}
;
......@@ -2137,6 +2151,7 @@ index_partition_cmd:
n->subtype = AT_AttachPartition;
cmd->name = $3;
cmd->bound = NULL;
cmd->concurrent = false;
n->def = (Node *) cmd;
$$ = (Node *) n;
......@@ -15395,6 +15410,7 @@ unreserved_keyword:
| EXTERNAL
| FAMILY
| FILTER
| FINALIZE
| FIRST_P
| FOLLOWING
| FORCE
......@@ -15936,6 +15952,7 @@ bare_label_keyword:
| EXTRACT
| FALSE_P
| FAMILY
| FINALIZE
| FIRST_P
| FLOAT_P
| FOLLOWING
......
......@@ -2798,7 +2798,7 @@ check_new_partition_bound(char *relname, Relation parent,
PartitionBoundSpec *spec, ParseState *pstate)
{
PartitionKey key = RelationGetPartitionKey(parent);
PartitionDesc partdesc = RelationGetPartitionDesc(parent);
PartitionDesc partdesc = RelationGetPartitionDesc(parent, true);
PartitionBoundInfo boundinfo = partdesc->boundinfo;
int with = -1;
bool overlap = false;
......@@ -3990,7 +3990,7 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
{
int i;
int ndatums = 0;
PartitionDesc pdesc = RelationGetPartitionDesc(parent);
PartitionDesc pdesc = RelationGetPartitionDesc(parent, true); /* XXX correct? */
PartitionBoundInfo boundinfo = pdesc->boundinfo;
if (boundinfo)
......@@ -4190,7 +4190,7 @@ get_qual_for_range(Relation parent, PartitionBoundSpec *spec,
if (spec->is_default)
{
List *or_expr_args = NIL;
PartitionDesc pdesc = RelationGetPartitionDesc(parent);
PartitionDesc pdesc = RelationGetPartitionDesc(parent, true); /* XXX correct? */
Oid *inhoids = pdesc->oids;
int nparts = pdesc->nparts,
i;
......
......@@ -37,6 +37,7 @@ typedef struct PartitionDirectoryData
{
MemoryContext pdir_mcxt;
HTAB *pdir_hash;
bool include_detached;
} PartitionDirectoryData;
typedef struct PartitionDirectoryEntry
......@@ -46,7 +47,7 @@ typedef struct PartitionDirectoryEntry
PartitionDesc pd;
} PartitionDirectoryEntry;
static void RelationBuildPartitionDesc(Relation rel);
static void RelationBuildPartitionDesc(Relation rel, bool include_detached);
/*
......@@ -61,13 +62,14 @@ static void RelationBuildPartitionDesc(Relation rel);
* that the data doesn't become stale.
*/
PartitionDesc
RelationGetPartitionDesc(Relation rel)
RelationGetPartitionDesc(Relation rel, bool include_detached)
{
if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
return NULL;
if (unlikely(rel->rd_partdesc == NULL))
RelationBuildPartitionDesc(rel);
if (unlikely(rel->rd_partdesc == NULL ||
rel->rd_partdesc->includes_detached != include_detached))
RelationBuildPartitionDesc(rel, include_detached);
return rel->rd_partdesc;
}
......@@ -88,7 +90,7 @@ RelationGetPartitionDesc(Relation rel)
* permanently.
*/
static void
RelationBuildPartitionDesc(Relation rel)
RelationBuildPartitionDesc(Relation rel, bool include_detached)
{
PartitionDesc partdesc;
PartitionBoundInfo boundinfo = NULL;
......@@ -110,7 +112,8 @@ RelationBuildPartitionDesc(Relation rel)
* concurrently, whatever this function returns will be accurate as of
* some well-defined point in time.
*/
inhoids = find_inheritance_children(RelationGetRelid(rel), NoLock);
inhoids = find_inheritance_children(RelationGetRelid(rel), include_detached,
NoLock);
nparts = list_length(inhoids);
/* Allocate working arrays for OIDs, leaf flags, and boundspecs. */
......@@ -238,6 +241,7 @@ RelationBuildPartitionDesc(Relation rel)
partdesc->boundinfo = partition_bounds_copy(boundinfo, key);
partdesc->oids = (Oid *) palloc(nparts * sizeof(Oid));
partdesc->is_leaf = (bool *) palloc(nparts * sizeof(bool));
partdesc->includes_detached = include_detached;
/*
* Assign OIDs from the original array into mapped indexes of the
......@@ -280,7 +284,7 @@ RelationBuildPartitionDesc(Relation rel)
* Create a new partition directory object.
*/
PartitionDirectory
CreatePartitionDirectory(MemoryContext mcxt)
CreatePartitionDirectory(MemoryContext mcxt, bool include_detached)
{
MemoryContext oldcontext = MemoryContextSwitchTo(mcxt);
PartitionDirectory pdir;
......@@ -295,6 +299,7 @@ CreatePartitionDirectory(MemoryContext mcxt)
pdir->pdir_hash = hash_create("partition directory", 256, &ctl,
HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
pdir->include_detached = include_detached;
MemoryContextSwitchTo(oldcontext);
return pdir;
......@@ -327,7 +332,7 @@ PartitionDirectoryLookup(PartitionDirectory pdir, Relation rel)
*/
RelationIncrementReferenceCount(rel);
pde->rel = rel;
pde->pd = RelationGetPartitionDesc(rel);
pde->pd = RelationGetPartitionDesc(rel, pdir->include_detached);
Assert(pde->pd != NULL);
}
return pde->pd;
......
......@@ -1236,6 +1236,25 @@ ProcessUtilitySlow(ParseState *pstate,
AlterTableStmt *atstmt = (AlterTableStmt *) parsetree;
Oid relid;
LOCKMODE lockmode;
ListCell *cell;
/*
* Disallow ALTER TABLE .. DETACH CONCURRENTLY in a
* transaction block or function. (Perhaps it could be
* allowed in a procedure, but don't hold your breath.)
*/
foreach(cell, atstmt->cmds)
{
AlterTableCmd *cmd = (AlterTableCmd *) lfirst(cell);
/* Disallow DETACH CONCURRENTLY in a transaction block */
if (cmd->subtype == AT_DetachPartition)
{
if (((PartitionCmd *) cmd->def)->concurrent)
PreventInTransactionBlock(isTopLevel,
"ALTER TABLE ... DETACH CONCURRENTLY");
}
}
/*
* Figure out lock mode, and acquire lock. This also does
......
......@@ -392,11 +392,15 @@ RI_FKey_check(TriggerData *trigdata)
/*
* Now check that foreign key exists in PK table
*
* XXX detectNewRows must be true when a partitioned table is on the
* referenced side. The reason is that our snapshot must be fresh
* in order for the hack in find_inheritance_children() to work.
*/
ri_PerformCheck(riinfo, &qkey, qplan,
fk_rel, pk_rel,
NULL, newslot,
false,
pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE,
SPI_OK_SELECT);
if (SPI_finish() != SPI_OK_FINISH)
......
......@@ -341,6 +341,7 @@ generate_partition_qual(Relation rel)
bool isnull;
List *my_qual = NIL,
*result = NIL;
Oid parentrelid;
Relation parent;
/* Guard against stack overflow due to overly deep partition tree */
......@@ -350,9 +351,14 @@ generate_partition_qual(Relation rel)
if (rel->rd_partcheckvalid)
return copyObject(rel->rd_partcheck);
/* Grab at least an AccessShareLock on the parent table */
parent = relation_open(get_partition_parent(RelationGetRelid(rel)),
AccessShareLock);
/*
* Grab at least an AccessShareLock on the parent table. Must do this
* even if the partition has been partially detached, because transactions
* concurrent with the detach might still be trying to use a partition
* descriptor that includes it.
*/
parentrelid = get_partition_parent(RelationGetRelid(rel), true);
parent = relation_open(parentrelid, AccessShareLock);
/* Get pg_class.relpartbound */
tuple = SearchSysCache1(RELOID, RelationGetRelid(rel));
......
......@@ -2144,7 +2144,12 @@ describeOneTableDetails(const char *schemaname,
printfPQExpBuffer(&buf,
"SELECT inhparent::pg_catalog.regclass,\n"
" pg_catalog.pg_get_expr(c.relpartbound, c.oid)");
" pg_catalog.pg_get_expr(c.relpartbound, c.oid),\n ");
appendPQExpBuffer(&buf,
pset.sversion >= 140000 ? "inhdetachpending" :
"false as inhdetachpending");
/* If verbose, also request the partition constraint definition */
if (verbose)
appendPQExpBufferStr(&buf,
......@@ -2162,17 +2167,19 @@ describeOneTableDetails(const char *schemaname,
{
char *parent_name = PQgetvalue(result, 0, 0);
char *partdef = PQgetvalue(result, 0, 1);
char *detached = PQgetvalue(result, 0, 2);
printfPQExpBuffer(&tmpbuf, _("Partition of: %s %s"), parent_name,
partdef);
printfPQExpBuffer(&tmpbuf, _("Partition of: %s %s%s"), parent_name,
partdef,
strcmp(detached, "t") == 0 ? " DETACH PENDING" : "");
printTableAddFooter(&cont, tmpbuf.data);
if (verbose)
{
char *partconstraintdef = NULL;
if (!PQgetisnull(result, 0, 2))
partconstraintdef = PQgetvalue(result, 0, 2);
if (!PQgetisnull(result, 0, 3))
partconstraintdef = PQgetvalue(result, 0, 3);
/* If there isn't any constraint, show that explicitly */
if (partconstraintdef == NULL || partconstraintdef[0] == '\0')
printfPQExpBuffer(&tmpbuf, _("No partition constraint"));
......@@ -3224,9 +3231,20 @@ describeOneTableDetails(const char *schemaname,
}
/* print child tables (with additional info if partitions) */
if (pset.sversion >= 100000)
if (pset.sversion >= 140000)
printfPQExpBuffer(&buf,
"SELECT c.oid::pg_catalog.regclass, c.relkind,"
" inhdetachpending,"
" pg_catalog.pg_get_expr(c.relpartbound, c.oid)\n"
"FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i\n"
"WHERE c.oid = i.inhrelid AND i.inhparent = '%s'\n"
"ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT',"
" c.oid::pg_catalog.regclass::pg_catalog.text;",
oid);
else if (pset.sversion >= 100000)
printfPQExpBuffer(&buf,
"SELECT c.oid::pg_catalog.regclass, c.relkind,"
" false AS inhdetachpending,"
" pg_catalog.pg_get_expr(c.relpartbound, c.oid)\n"
"FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i\n"
"WHERE c.oid = i.inhrelid AND i.inhparent = '%s'\n"
......@@ -3235,14 +3253,16 @@ describeOneTableDetails(const char *schemaname,
oid);
else if (pset.sversion >= 80300)
printfPQExpBuffer(&buf,
"SELECT c.oid::pg_catalog.regclass, c.relkind, NULL\n"
"SELECT c.oid::pg_catalog.regclass, c.relkind,"
" false AS inhdetachpending, NULL\n"
"FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i\n"
"WHERE c.oid = i.inhrelid AND i.inhparent = '%s'\n"
"ORDER BY c.oid::pg_catalog.regclass::pg_catalog.text;",
oid);
else
printfPQExpBuffer(&buf,
"SELECT c.oid::pg_catalog.regclass, c.relkind, NULL\n"
"SELECT c.oid::pg_catalog.regclass, c.relkind,"
" false AS inhdetachpending, NULL\n"
"FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i\n"
"WHERE c.oid = i.inhrelid AND i.inhparent = '%s'\n"
"ORDER BY c.relname;",
......@@ -3292,11 +3312,13 @@ describeOneTableDetails(const char *schemaname,
else
printfPQExpBuffer(&buf, "%*s %s",
ctw, "", PQgetvalue(result, i, 0));
if (!PQgetisnull(result, i, 2))
appendPQExpBuffer(&buf, " %s", PQgetvalue(result, i, 2));
if (!PQgetisnull(result, i, 3))
appendPQExpBuffer(&buf, " %s", PQgetvalue(result, i, 3));
if (child_relkind == RELKIND_PARTITIONED_TABLE ||
child_relkind == RELKIND_PARTITIONED_INDEX)
appendPQExpBufferStr(&buf, ", PARTITIONED");
if (strcmp(PQgetvalue(result, i, 2), "t") == 0)
appendPQExpBuffer(&buf, " (DETACH PENDING)");
if (i < tuples - 1)
appendPQExpBufferChar(&buf, ',');
......
......@@ -53,6 +53,6 @@
*/
/* yyyymmddN */
#define CATALOG_VERSION_NO 202103242
#define CATALOG_VERSION_NO 202103251
#endif
......@@ -19,7 +19,7 @@
/* Seed for the extended hash function */
#define HASH_PARTITION_SEED UINT64CONST(0x7A5B22367996DCFD)
extern Oid get_partition_parent(Oid relid);
extern Oid get_partition_parent(Oid relid, bool even_if_detached);
extern List *get_partition_ancestors(Oid relid);
extern Oid index_get_partition(Relation partition, Oid indexId);
extern List *map_partition_varattnos(List *expr, int fromrel_varno,
......
......@@ -34,6 +34,7 @@ CATALOG(pg_inherits,2611,InheritsRelationId)
Oid inhrelid BKI_LOOKUP(pg_class);
Oid inhparent BKI_LOOKUP(pg_class);
int32 inhseqno;
bool inhdetachpending;
} FormData_pg_inherits;
/* ----------------
......@@ -49,7 +50,8 @@ DECLARE_INDEX(pg_inherits_parent_index, 2187, on pg_inherits using btree(inhpare
#define InheritsParentIndexId 2187
extern List *find_inheritance_children(Oid parentrelId, LOCKMODE lockmode);
extern List *find_inheritance_children(Oid parentrelId, bool include_detached,
LOCKMODE lockmode);
extern List *find_all_inheritors(Oid parentrelId, LOCKMODE lockmode,
List **parents);
extern bool has_subclass(Oid relationId);
......@@ -57,6 +59,8 @@ extern bool has_superclass(Oid relationId);
extern bool typeInheritsFrom(Oid subclassTypeId, Oid superclassTypeId);
extern void StoreSingleInheritance(Oid relationId, Oid parentOid,
int32 seqNumber);
extern bool DeleteInheritsTuple(Oid inhrelid, Oid inhparent);
extern bool DeleteInheritsTuple(Oid inhrelid, Oid inhparent, bool allow_detached,
const char *childname);
extern bool PartitionHasPendingDetach(Oid partoid);
#endif /* PG_INHERITS_H */
......@@ -871,6 +871,7 @@ typedef struct PartitionCmd
NodeTag type;
RangeVar *name; /* name of partition to attach/detach */
PartitionBoundSpec *bound; /* FOR VALUES, if attaching */
bool concurrent;
} PartitionCmd;
/****************************************************************************
......@@ -1909,6 +1910,7 @@ typedef enum AlterTableType
AT_GenericOptions, /* OPTIONS (...) */
AT_AttachPartition, /* ATTACH PARTITION */
AT_DetachPartition, /* DETACH PARTITION */
AT_DetachPartitionFinalize, /* DETACH PARTITION FINALIZE */
AT_AddIdentity, /* ADD IDENTITY */
AT_SetIdentity, /* SET identity column options */
AT_DropIdentity, /* DROP IDENTITY */
......
......@@ -166,6 +166,7 @@ PG_KEYWORD("false", FALSE_P, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("family", FAMILY, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("fetch", FETCH, RESERVED_KEYWORD, AS_LABEL)
PG_KEYWORD("filter", FILTER, UNRESERVED_KEYWORD, AS_LABEL)
PG_KEYWORD("finalize", FINALIZE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("first", FIRST_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("float", FLOAT_P, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("following", FOLLOWING, UNRESERVED_KEYWORD, BARE_LABEL)
......
......@@ -21,6 +21,7 @@
typedef struct PartitionDescData
{
int nparts; /* Number of partitions */
bool includes_detached; /* Does it include detached partitions */
Oid *oids; /* Array of 'nparts' elements containing
* partition OIDs in order of the their bounds */
bool *is_leaf; /* Array of 'nparts' elements storing whether
......@@ -30,9 +31,9 @@ typedef struct PartitionDescData
} PartitionDescData;
extern PartitionDesc RelationGetPartitionDesc(Relation rel);
extern PartitionDesc RelationGetPartitionDesc(Relation rel, bool include_detached);
extern PartitionDirectory CreatePartitionDirectory(MemoryContext mcxt);
extern PartitionDirectory CreatePartitionDirectory(MemoryContext mcxt, bool include_detached);
extern PartitionDesc PartitionDirectoryLookup(PartitionDirectory, Relation);
extern void DestroyPartitionDirectory(PartitionDirectory pdir);
......
......@@ -132,6 +132,7 @@ extern void AtEOXact_Snapshot(bool isCommit, bool resetXmin);
extern void ImportSnapshot(const char *idstr);
extern bool XactHasExportedSnapshots(void);
extern void DeleteAllExportedSnapshotFiles(void);
extern void WaitForOlderSnapshots(TransactionId limitXmin, bool progress);
extern bool ThereAreNoPriorRegisteredSnapshots(void);
extern bool TransactionIdLimitedForOldSnapshots(TransactionId recentXmin,
Relation relation,
......
Parsed test spec with 3 sessions
starting permutation: s1b s1s s2detach s1s s1c s1s
step s1b: BEGIN;
step s1s: SELECT * FROM d_listp;
a
1
2
step s2detach: ALTER TABLE d_listp DETACH PARTITION d_listp2 CONCURRENTLY; <waiting ...>
step s1s: SELECT * FROM d_listp;
a
1
step s1c: COMMIT;
step s2detach: <... completed>
step s1s: SELECT * FROM d_listp;
a
1
starting permutation: s1b s1s s2detach s1s s3s s3i s1c s3i s2drop s1s
step s1b: BEGIN;
step s1s: SELECT * FROM d_listp;
a
1
2
step s2detach: ALTER TABLE d_listp DETACH PARTITION d_listp2 CONCURRENTLY; <waiting ...>
step s1s: SELECT * FROM d_listp;
a
1
step s3s: SELECT * FROM d_listp;
a
1
step s3i: SELECT relpartbound IS NULL FROM pg_class where relname = 'd_listp2';
?column?
f
step s1c: COMMIT;
step s2detach: <... completed>
step s3i: SELECT relpartbound IS NULL FROM pg_class where relname = 'd_listp2';
?column?
t
step s2drop: DROP TABLE d_listp2;
step s1s: SELECT * FROM d_listp;
a
1
starting permutation: s1b s1s s2detach s1ins s1s s1c
step s1b: BEGIN;
step s1s: SELECT * FROM d_listp;
a
1
2
step s2detach: ALTER TABLE d_listp DETACH PARTITION d_listp2 CONCURRENTLY; <waiting ...>
step s1ins: INSERT INTO d_listp VALUES (1);
step s1s: SELECT * FROM d_listp;
a
1
1
step s1c: COMMIT;
step s2detach: <... completed>
starting permutation: s1b s1s s1ins2 s2detach s1ins s1s s1c
step s1b: BEGIN;
step s1s: SELECT * FROM d_listp;
a
1
2
step s1ins2: INSERT INTO d_listp VALUES (2);
step s2detach: ALTER TABLE d_listp DETACH PARTITION d_listp2 CONCURRENTLY; <waiting ...>
step s1ins: INSERT INTO d_listp VALUES (1);
step s1s: SELECT * FROM d_listp;
a
1
1
step s1c: COMMIT;
step s2detach: <... completed>
starting permutation: s1brr s1s s2detach s1ins s1s s1c
step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
step s1s: SELECT * FROM d_listp;
a
1
2
step s2detach: ALTER TABLE d_listp DETACH PARTITION d_listp2 CONCURRENTLY; <waiting ...>
step s1ins: INSERT INTO d_listp VALUES (1);
step s1s: SELECT * FROM d_listp;
a
1
1
2
step s1c: COMMIT;
step s2detach: <... completed>
starting permutation: s1brr s1s s2detach s1s s1c
step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
step s1s: SELECT * FROM d_listp;
a
1
2
step s2detach: ALTER TABLE d_listp DETACH PARTITION d_listp2 CONCURRENTLY; <waiting ...>
step s1s: SELECT * FROM d_listp;
a
1
2
step s1c: COMMIT;
step s2detach: <... completed>
starting permutation: s1b s1ins2 s2detach s3ins2 s1c
step s1b: BEGIN;
step s1ins2: INSERT INTO d_listp VALUES (2);
step s2detach: ALTER TABLE d_listp DETACH PARTITION d_listp2 CONCURRENTLY; <waiting ...>
step s3ins2: INSERT INTO d_listp VALUES (2);
ERROR: no partition of relation "d_listp" found for row
step s1c: COMMIT;
step s2detach: <... completed>
starting permutation: s1brr s1prep s1s s2detach s1s s1exec1 s3s s1dealloc s1c
step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
step s1prep: PREPARE f(int) AS INSERT INTO d_listp VALUES ($1);
step s1s: SELECT * FROM d_listp;
a
1
2
step s2detach: ALTER TABLE d_listp DETACH PARTITION d_listp2 CONCURRENTLY; <waiting ...>
step s1s: SELECT * FROM d_listp;
a
1
2
step s1exec1: EXECUTE f(1);
step s3s: SELECT * FROM d_listp;
a
1
step s1dealloc: DEALLOCATE f;
step s1c: COMMIT;
step s2detach: <... completed>
starting permutation: s1brr s1prep s1exec2 s2detach s1s s1exec2 s3s s1c s1dealloc
step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
step s1prep: PREPARE f(int) AS INSERT INTO d_listp VALUES ($1);
step s1exec2: EXECUTE f(2);
step s2detach: ALTER TABLE d_listp DETACH PARTITION d_listp2 CONCURRENTLY; <waiting ...>
step s1s: SELECT * FROM d_listp;
a
1
2
2
step s1exec2: EXECUTE f(2);
step s3s: SELECT * FROM d_listp;
a
1
step s1c: COMMIT;
step s2detach: <... completed>
step s1dealloc: DEALLOCATE f;
starting permutation: s1brr s1prep s1s s2detach s1s s1exec2 s1c s1dealloc
step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
step s1prep: PREPARE f(int) AS INSERT INTO d_listp VALUES ($1);
step s1s: SELECT * FROM d_listp;
a
1
2
step s2detach: ALTER TABLE d_listp DETACH PARTITION d_listp2 CONCURRENTLY; <waiting ...>
step s1s: SELECT * FROM d_listp;
a
1
2
step s1exec2: EXECUTE f(2);
step s1c: COMMIT;
step s2detach: <... completed>
step s1dealloc: DEALLOCATE f;
starting permutation: s1brr s1prep s2detach s1s s1exec2 s1c s1dealloc
step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
step s1prep: PREPARE f(int) AS INSERT INTO d_listp VALUES ($1);
step s2detach: ALTER TABLE d_listp DETACH PARTITION d_listp2 CONCURRENTLY; <waiting ...>
step s1s: SELECT * FROM d_listp;
a
1
2
step s1exec2: EXECUTE f(2);
step s1c: COMMIT;
step s2detach: <... completed>
step s1dealloc: DEALLOCATE f;
starting permutation: s1brr s1prep1 s2detach s1s s1exec2 s1c s1dealloc
step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
step s1prep1: PREPARE f(int) AS INSERT INTO d_listp VALUES (1);
step s2detach: ALTER TABLE d_listp DETACH PARTITION d_listp2 CONCURRENTLY; <waiting ...>
step s1s: SELECT * FROM d_listp;
a
1
2
step s1exec2: EXECUTE f(2);
step s1c: COMMIT;
step s2detach: <... completed>
step s1dealloc: DEALLOCATE f;
starting permutation: s1brr s1prep2 s2detach s1s s1exec2 s1c s1dealloc
step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
step s1prep2: PREPARE f(int) AS INSERT INTO d_listp VALUES (2);
step s2detach: ALTER TABLE d_listp DETACH PARTITION d_listp2 CONCURRENTLY; <waiting ...>
step s1s: SELECT * FROM d_listp;
a
1
2
step s1exec2: EXECUTE f(2);
step s1c: COMMIT;
step s2detach: <... completed>
step s1dealloc: DEALLOCATE f;
Parsed test spec with 3 sessions
starting permutation: s1b s1s s2d s3i1 s1c
step s1b: BEGIN;
step s1s: SELECT * FROM d_lp_fk;
a
1
2
step s2d: ALTER TABLE d_lp_fk DETACH PARTITION d_lp_fk_1 CONCURRENTLY; <waiting ...>
step s3i1: INSERT INTO d_lp_fk_r VALUES (1);
ERROR: insert or update on table "d_lp_fk_r" violates foreign key constraint "d_lp_fk_r_a_fkey"
step s1c: COMMIT;
step s2d: <... completed>
starting permutation: s1b s1s s2d s3i2 s3i2 s1c
step s1b: BEGIN;
step s1s: SELECT * FROM d_lp_fk;
a
1
2
step s2d: ALTER TABLE d_lp_fk DETACH PARTITION d_lp_fk_1 CONCURRENTLY; <waiting ...>
step s3i2: INSERT INTO d_lp_fk_r VALUES (2);
step s3i2: INSERT INTO d_lp_fk_r VALUES (2);
step s1c: COMMIT;
step s2d: <... completed>
starting permutation: s1b s1s s3i1 s2d s1c
step s1b: BEGIN;
step s1s: SELECT * FROM d_lp_fk;
a
1
2
step s3i1: INSERT INTO d_lp_fk_r VALUES (1);
step s2d: ALTER TABLE d_lp_fk DETACH PARTITION d_lp_fk_1 CONCURRENTLY;
ERROR: removing partition "d_lp_fk_1" violates foreign key constraint "d_lp_fk_r_a_fkey1"
step s1c: COMMIT;
starting permutation: s1b s1s s3i2 s2d s1c
step s1b: BEGIN;
step s1s: SELECT * FROM d_lp_fk;
a
1
2
step s3i2: INSERT INTO d_lp_fk_r VALUES (2);
step s2d: ALTER TABLE d_lp_fk DETACH PARTITION d_lp_fk_1 CONCURRENTLY; <waiting ...>
step s1c: COMMIT;
step s2d: <... completed>
starting permutation: s1b s1s s3b s2d s3i1 s1c s3c
step s1b: BEGIN;
step s1s: SELECT * FROM d_lp_fk;
a
1
2
step s3b: BEGIN;
step s2d: ALTER TABLE d_lp_fk DETACH PARTITION d_lp_fk_1 CONCURRENTLY; <waiting ...>
step s3i1: INSERT INTO d_lp_fk_r VALUES (1);
ERROR: insert or update on table "d_lp_fk_r" violates foreign key constraint "d_lp_fk_r_a_fkey"
step s1c: COMMIT;
step s2d: <... completed>
step s3c: COMMIT;
Parsed test spec with 2 sessions
starting permutation: s2snitch s1b s1s s2detach s1cancel s1c s1describe s1alter
step s2snitch: INSERT INTO d3_pid SELECT pg_backend_pid();
step s1b: BEGIN;
step s1s: SELECT * FROM d3_listp;
a
1
step s2detach: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 CONCURRENTLY; <waiting ...>
step s1cancel: SELECT pg_cancel_backend(pid) FROM d3_pid;
pg_cancel_backend
t
step s2detach: <... completed>
error in steps s1cancel s2detach: ERROR: canceling statement due to user request
step s1c: COMMIT;
step s1describe: SELECT 'd3_listp' AS root, * FROM pg_partition_tree('d3_listp')
UNION ALL SELECT 'd3_listp1', * FROM pg_partition_tree('d3_listp1');
root relid parentrelid isleaf level
d3_listp d3_listp f 0
d3_listp1 d3_listp1 t 0
step s1alter: ALTER TABLE d3_listp1 ALTER a DROP NOT NULL;
ERROR: cannot alter partition "d3_listp1" with an incomplete detach
starting permutation: s2snitch s1b s1s s2detach s1cancel s1insert s1c
step s2snitch: INSERT INTO d3_pid SELECT pg_backend_pid();
step s1b: BEGIN;
step s1s: SELECT * FROM d3_listp;
a
1
step s2detach: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 CONCURRENTLY; <waiting ...>
step s1cancel: SELECT pg_cancel_backend(pid) FROM d3_pid;
pg_cancel_backend
t
step s2detach: <... completed>
error in steps s1cancel s2detach: ERROR: canceling statement due to user request
step s1insert: INSERT INTO d3_listp VALUES (1);
ERROR: no partition of relation "d3_listp" found for row
step s1c: COMMIT;
starting permutation: s2snitch s1brr s1s s2detach s1cancel s1insert s1c s1spart
step s2snitch: INSERT INTO d3_pid SELECT pg_backend_pid();
step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
step s1s: SELECT * FROM d3_listp;
a
1
step s2detach: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 CONCURRENTLY; <waiting ...>
step s1cancel: SELECT pg_cancel_backend(pid) FROM d3_pid;
pg_cancel_backend
t
step s2detach: <... completed>
error in steps s1cancel s2detach: ERROR: canceling statement due to user request
step s1insert: INSERT INTO d3_listp VALUES (1);
step s1c: COMMIT;
step s1spart: SELECT * FROM d3_listp1;
a
1
1
starting permutation: s2snitch s1b s1s s2detach s1cancel s1c s1insertpart
step s2snitch: INSERT INTO d3_pid SELECT pg_backend_pid();
step s1b: BEGIN;
step s1s: SELECT * FROM d3_listp;
a
1
step s2detach: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 CONCURRENTLY; <waiting ...>
step s1cancel: SELECT pg_cancel_backend(pid) FROM d3_pid;
pg_cancel_backend
t
step s2detach: <... completed>
error in steps s1cancel s2detach: ERROR: canceling statement due to user request
step s1c: COMMIT;
step s1insertpart: INSERT INTO d3_listp1 VALUES (1);
starting permutation: s2snitch s1b s1s s2detach s1cancel s1c s1drop s1list
step s2snitch: INSERT INTO d3_pid SELECT pg_backend_pid();
step s1b: BEGIN;
step s1s: SELECT * FROM d3_listp;
a
1
step s2detach: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 CONCURRENTLY; <waiting ...>
step s1cancel: SELECT pg_cancel_backend(pid) FROM d3_pid;
pg_cancel_backend
t
step s2detach: <... completed>
error in steps s1cancel s2detach: ERROR: canceling statement due to user request
step s1c: COMMIT;
step s1drop: DROP TABLE d3_listp;
step s1list: SELECT relname FROM pg_catalog.pg_class
WHERE relname LIKE 'd3_listp%' ORDER BY 1;
relname
starting permutation: s2snitch s1b s1s s2detach s1cancel s1c s1trunc s1spart
step s2snitch: INSERT INTO d3_pid SELECT pg_backend_pid();
step s1b: BEGIN;
step s1s: SELECT * FROM d3_listp;
a
1
step s2detach: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 CONCURRENTLY; <waiting ...>
step s1cancel: SELECT pg_cancel_backend(pid) FROM d3_pid;
pg_cancel_backend
t
step s2detach: <... completed>
error in steps s1cancel s2detach: ERROR: canceling statement due to user request
step s1c: COMMIT;
step s1trunc: TRUNCATE TABLE d3_listp;
step s1spart: SELECT * FROM d3_listp1;
a
1
starting permutation: s2snitch s1b s1s s2detach s1cancel s1c s2begin s2drop s1s s2commit
step s2snitch: INSERT INTO d3_pid SELECT pg_backend_pid();
step s1b: BEGIN;
step s1s: SELECT * FROM d3_listp;
a
1
step s2detach: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 CONCURRENTLY; <waiting ...>
step s1cancel: SELECT pg_cancel_backend(pid) FROM d3_pid;
pg_cancel_backend
t
step s2detach: <... completed>
error in steps s1cancel s2detach: ERROR: canceling statement due to user request
step s1c: COMMIT;
step s2begin: BEGIN;
step s2drop: DROP TABLE d3_listp1;
step s1s: SELECT * FROM d3_listp; <waiting ...>
step s2commit: COMMIT;
step s1s: <... completed>
a
starting permutation: s2snitch s1b s1s s2detach s1cancel s1c s1b s1spart s2detachfinal s1c
step s2snitch: INSERT INTO d3_pid SELECT pg_backend_pid();
step s1b: BEGIN;
step s1s: SELECT * FROM d3_listp;
a
1
step s2detach: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 CONCURRENTLY; <waiting ...>
step s1cancel: SELECT pg_cancel_backend(pid) FROM d3_pid;
pg_cancel_backend
t
step s2detach: <... completed>
error in steps s1cancel s2detach: ERROR: canceling statement due to user request
step s1c: COMMIT;
step s1b: BEGIN;
step s1spart: SELECT * FROM d3_listp1;
a
1
step s2detachfinal: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 FINALIZE; <waiting ...>
step s1c: COMMIT;
step s2detachfinal: <... completed>
starting permutation: s2snitch s1b s1s s2detach s1cancel s1c s1b s1s s2detachfinal s1c
step s2snitch: INSERT INTO d3_pid SELECT pg_backend_pid();
step s1b: BEGIN;
step s1s: SELECT * FROM d3_listp;
a
1
step s2detach: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 CONCURRENTLY; <waiting ...>
step s1cancel: SELECT pg_cancel_backend(pid) FROM d3_pid;
pg_cancel_backend
t
step s2detach: <... completed>
error in steps s1cancel s2detach: ERROR: canceling statement due to user request
step s1c: COMMIT;
step s1b: BEGIN;
step s1s: SELECT * FROM d3_listp;
a
step s2detachfinal: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 FINALIZE;
step s1c: COMMIT;
starting permutation: s2snitch s1b s1s s2detach s1cancel s1c s1b s1spart s2detachfinal s1c
step s2snitch: INSERT INTO d3_pid SELECT pg_backend_pid();
step s1b: BEGIN;
step s1s: SELECT * FROM d3_listp;
a
1
step s2detach: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 CONCURRENTLY; <waiting ...>
step s1cancel: SELECT pg_cancel_backend(pid) FROM d3_pid;
pg_cancel_backend
t
step s2detach: <... completed>
error in steps s1cancel s2detach: ERROR: canceling statement due to user request
step s1c: COMMIT;
step s1b: BEGIN;
step s1spart: SELECT * FROM d3_listp1;
a
1
step s2detachfinal: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 FINALIZE; <waiting ...>
step s1c: COMMIT;
step s2detachfinal: <... completed>
starting permutation: s2snitch s1b s1s s2detach s1cancel s1c s2begin s2detachfinal s2commit
step s2snitch: INSERT INTO d3_pid SELECT pg_backend_pid();
step s1b: BEGIN;
step s1s: SELECT * FROM d3_listp;
a
1
step s2detach: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 CONCURRENTLY; <waiting ...>
step s1cancel: SELECT pg_cancel_backend(pid) FROM d3_pid;
pg_cancel_backend
t
step s2detach: <... completed>
error in steps s1cancel s2detach: ERROR: canceling statement due to user request
step s1c: COMMIT;
step s2begin: BEGIN;
step s2detachfinal: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 FINALIZE;
step s2commit: COMMIT;
starting permutation: s2snitch s1b s1s s2detach s1cancel s1c s2begin s2detachfinal s1spart s2commit
step s2snitch: INSERT INTO d3_pid SELECT pg_backend_pid();
step s1b: BEGIN;
step s1s: SELECT * FROM d3_listp;
a
1
step s2detach: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 CONCURRENTLY; <waiting ...>
step s1cancel: SELECT pg_cancel_backend(pid) FROM d3_pid;
pg_cancel_backend
t
step s2detach: <... completed>
error in steps s1cancel s2detach: ERROR: canceling statement due to user request
step s1c: COMMIT;
step s2begin: BEGIN;
step s2detachfinal: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 FINALIZE;
step s1spart: SELECT * FROM d3_listp1; <waiting ...>
step s2commit: COMMIT;
step s1spart: <... completed>
a
1
starting permutation: s2snitch s1b s1s s2detach s1cancel s1c s2begin s2detachfinal s1insertpart s2commit
step s2snitch: INSERT INTO d3_pid SELECT pg_backend_pid();
step s1b: BEGIN;
step s1s: SELECT * FROM d3_listp;
a
1
step s2detach: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 CONCURRENTLY; <waiting ...>
step s1cancel: SELECT pg_cancel_backend(pid) FROM d3_pid;
pg_cancel_backend
t
step s2detach: <... completed>
error in steps s1cancel s2detach: ERROR: canceling statement due to user request
step s1c: COMMIT;
step s2begin: BEGIN;
step s2detachfinal: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 FINALIZE;
step s1insertpart: INSERT INTO d3_listp1 VALUES (1); <waiting ...>
step s2commit: COMMIT;
step s1insertpart: <... completed>
unused step name: s1droppart
......@@ -24,6 +24,10 @@ test: deadlock-hard
test: deadlock-soft
test: deadlock-soft-2
test: deadlock-parallel
test: detach-partition-concurrently-1
test: detach-partition-concurrently-2
test: detach-partition-concurrently-3
test: detach-partition-concurrently-4
test: fk-contention
test: fk-deadlock
test: fk-deadlock2
......
# Test that detach partition concurrently makes the partition invisible at the
# correct time.
setup
{
DROP TABLE IF EXISTS d_listp, d_listp1, d_listp2;
CREATE TABLE d_listp (a int) PARTITION BY LIST(a);
CREATE TABLE d_listp1 PARTITION OF d_listp FOR VALUES IN (1);
CREATE TABLE d_listp2 PARTITION OF d_listp FOR VALUES IN (2);
INSERT INTO d_listp VALUES (1),(2);
}
teardown {
DROP TABLE IF EXISTS d_listp, d_listp2, d_listp_foobar;
}
session "s1"
step "s1b" { BEGIN; }
step "s1brr" { BEGIN ISOLATION LEVEL REPEATABLE READ; }
step "s1s" { SELECT * FROM d_listp; }
step "s1ins" { INSERT INTO d_listp VALUES (1); }
step "s1ins2" { INSERT INTO d_listp VALUES (2); }
step "s1prep" { PREPARE f(int) AS INSERT INTO d_listp VALUES ($1); }
step "s1prep1" { PREPARE f(int) AS INSERT INTO d_listp VALUES (1); }
step "s1prep2" { PREPARE f(int) AS INSERT INTO d_listp VALUES (2); }
step "s1exec1" { EXECUTE f(1); }
step "s1exec2" { EXECUTE f(2); }
step "s1dealloc" { DEALLOCATE f; }
step "s1c" { COMMIT; }
session "s2"
step "s2detach" { ALTER TABLE d_listp DETACH PARTITION d_listp2 CONCURRENTLY; }
step "s2drop" { DROP TABLE d_listp2; }
session "s3"
step "s3s" { SELECT * FROM d_listp; }
step "s3i" { SELECT relpartbound IS NULL FROM pg_class where relname = 'd_listp2'; }
step "s3ins2" { INSERT INTO d_listp VALUES (2); }
# The transaction that detaches hangs until it sees any older transaction
# terminate, as does anybody else.
permutation "s1b" "s1s" "s2detach" "s1s" "s1c" "s1s"
# relpartbound remains set until s1 commits
# XXX this could be timing dependent :-(
permutation "s1b" "s1s" "s2detach" "s1s" "s3s" "s3i" "s1c" "s3i" "s2drop" "s1s"
# In read-committed mode, the partition disappears from view of concurrent
# transactions immediately. But if a write lock is held, then the detach
# has to wait.
permutation "s1b" "s1s" "s2detach" "s1ins" "s1s" "s1c"
permutation "s1b" "s1s" "s1ins2" "s2detach" "s1ins" "s1s" "s1c"
# In repeatable-read mode, the partition remains visible until commit even
# if the to-be-detached partition is not locked for write.
permutation "s1brr" "s1s" "s2detach" "s1ins" "s1s" "s1c"
permutation "s1brr" "s1s" "s2detach" "s1s" "s1c"
# Another process trying to acquire a write lock will be blocked behind the
# detacher
permutation "s1b" "s1ins2" "s2detach" "s3ins2" "s1c"
# a prepared query is not blocked
permutation "s1brr" "s1prep" "s1s" "s2detach" "s1s" "s1exec1" "s3s" "s1dealloc" "s1c"
permutation "s1brr" "s1prep" "s1exec2" "s2detach" "s1s" "s1exec2" "s3s" "s1c" "s1dealloc"
permutation "s1brr" "s1prep" "s1s" "s2detach" "s1s" "s1exec2" "s1c" "s1dealloc"
permutation "s1brr" "s1prep" "s2detach" "s1s" "s1exec2" "s1c" "s1dealloc"
permutation "s1brr" "s1prep1" "s2detach" "s1s" "s1exec2" "s1c" "s1dealloc"
permutation "s1brr" "s1prep2" "s2detach" "s1s" "s1exec2" "s1c" "s1dealloc"
# Test that detach partition concurrently makes the partition safe
# for foreign keys that reference it.
setup
{
DROP TABLE IF EXISTS d_lp_fk, d_lp_fk_1, d_lp_fk_2, d_lp_fk_r;
CREATE TABLE d_lp_fk (a int PRIMARY KEY) PARTITION BY LIST(a);
CREATE TABLE d_lp_fk_1 PARTITION OF d_lp_fk FOR VALUES IN (1);
CREATE TABLE d_lp_fk_2 PARTITION OF d_lp_fk FOR VALUES IN (2);
INSERT INTO d_lp_fk VALUES (1), (2);
CREATE TABLE d_lp_fk_r (a int references d_lp_fk);
}
teardown { DROP TABLE IF EXISTS d_lp_fk, d_lp_fk_1, d_lp_fk_2, d_lp_fk_r; }
session "s1"
step "s1b" { BEGIN; }
step "s1s" { SELECT * FROM d_lp_fk; }
step "s1c" { COMMIT; }
session "s2"
step "s2d" { ALTER TABLE d_lp_fk DETACH PARTITION d_lp_fk_1 CONCURRENTLY; }
session "s3"
step "s3b" { BEGIN; }
step "s3i1" { INSERT INTO d_lp_fk_r VALUES (1); }
step "s3i2" { INSERT INTO d_lp_fk_r VALUES (2); }
step "s3c" { COMMIT; }
# The transaction that detaches hangs until it sees any older transaction
# terminate.
permutation "s1b" "s1s" "s2d" "s3i1" "s1c"
permutation "s1b" "s1s" "s2d" "s3i2" "s3i2" "s1c"
permutation "s1b" "s1s" "s3i1" "s2d" "s1c"
permutation "s1b" "s1s" "s3i2" "s2d" "s1c"
# what if s3 has an uncommitted insertion?
permutation "s1b" "s1s" "s3b" "s2d" "s3i1" "s1c" "s3c"
# Try various things to happen to a partition with an incomplete detach
setup
{
CREATE TABLE d3_listp (a int) PARTITION BY LIST(a);
CREATE TABLE d3_listp1 PARTITION OF d3_listp FOR VALUES IN (1);
CREATE TABLE d3_pid (pid int);
INSERT INTO d3_listp VALUES (1);
}
teardown {
DROP TABLE IF EXISTS d3_listp, d3_listp1, d3_pid;
}
session "s1"
step "s1b" { BEGIN; }
step "s1brr" { BEGIN ISOLATION LEVEL REPEATABLE READ; }
step "s1s" { SELECT * FROM d3_listp; }
step "s1spart" { SELECT * FROM d3_listp1; }
step "s1cancel" { SELECT pg_cancel_backend(pid) FROM d3_pid; }
step "s1c" { COMMIT; }
step "s1alter" { ALTER TABLE d3_listp1 ALTER a DROP NOT NULL; }
step "s1insert" { INSERT INTO d3_listp VALUES (1); }
step "s1insertpart" { INSERT INTO d3_listp1 VALUES (1); }
step "s1drop" { DROP TABLE d3_listp; }
step "s1droppart" { DROP TABLE d3_listp1; }
step "s1trunc" { TRUNCATE TABLE d3_listp; }
step "s1list" { SELECT relname FROM pg_catalog.pg_class
WHERE relname LIKE 'd3_listp%' ORDER BY 1; }
step "s1describe" { SELECT 'd3_listp' AS root, * FROM pg_partition_tree('d3_listp')
UNION ALL SELECT 'd3_listp1', * FROM pg_partition_tree('d3_listp1'); }
session "s2"
step "s2begin" { BEGIN; }
step "s2snitch" { INSERT INTO d3_pid SELECT pg_backend_pid(); }
step "s2detach" { ALTER TABLE d3_listp DETACH PARTITION d3_listp1 CONCURRENTLY; }
step "s2detachfinal" { ALTER TABLE d3_listp DETACH PARTITION d3_listp1 FINALIZE; }
step "s2drop" { DROP TABLE d3_listp1; }
step "s2commit" { COMMIT; }
# Try various things while the partition is in "being detached" state, with
# no session waiting.
permutation "s2snitch" "s1b" "s1s" "s2detach" "s1cancel" "s1c" "s1describe" "s1alter"
permutation "s2snitch" "s1b" "s1s" "s2detach" "s1cancel" "s1insert" "s1c"
permutation "s2snitch" "s1brr" "s1s" "s2detach" "s1cancel" "s1insert" "s1c" "s1spart"
permutation "s2snitch" "s1b" "s1s" "s2detach" "s1cancel" "s1c" "s1insertpart"
# "drop" here does both tables
permutation "s2snitch" "s1b" "s1s" "s2detach" "s1cancel" "s1c" "s1drop" "s1list"
# "truncate" only does parent, not partition
permutation "s2snitch" "s1b" "s1s" "s2detach" "s1cancel" "s1c" "s1trunc" "s1spart"
# When a partition with incomplete detach is dropped, we grab lock on parent too.
permutation "s2snitch" "s1b" "s1s" "s2detach" "s1cancel" "s1c" "s2begin" "s2drop" "s1s" "s2commit"
# Partially detach, then select and try to complete the detach. Reading
# from partition blocks (AEL is required on partition); reading from parent
# does not block.
permutation "s2snitch" "s1b" "s1s" "s2detach" "s1cancel" "s1c" "s1b" "s1spart" "s2detachfinal" "s1c"
permutation "s2snitch" "s1b" "s1s" "s2detach" "s1cancel" "s1c" "s1b" "s1s" "s2detachfinal" "s1c"
# DETACH FINALIZE in a transaction block. No insert/select on the partition
# is allowed concurrently with that.
permutation "s2snitch" "s1b" "s1s" "s2detach" "s1cancel" "s1c" "s1b" "s1spart" "s2detachfinal" "s1c"
permutation "s2snitch" "s1b" "s1s" "s2detach" "s1cancel" "s1c" "s2begin" "s2detachfinal" "s2commit"
permutation "s2snitch" "s1b" "s1s" "s2detach" "s1cancel" "s1c" "s2begin" "s2detachfinal" "s1spart" "s2commit"
permutation "s2snitch" "s1b" "s1s" "s2detach" "s1cancel" "s1c" "s2begin" "s2detachfinal" "s1insertpart" "s2commit"
# This test exercises behavior of foreign keys in the face of concurrent
# detach of partitions in the referenced table.
# (The cases where the detaching transaction is cancelled is interesting
# because the locking situation is completely different. I didn't verify
# that keeping both variants adds any extra coverage.)
setup {
drop table if exists d4_primary, d4_primary1, d4_fk, d4_pid;
create table d4_primary (a int primary key) partition by list (a);
create table d4_primary1 partition of d4_primary for values in (1);
create table d4_primary2 partition of d4_primary for values in (2);
insert into d4_primary values (1);
insert into d4_primary values (2);
create table d4_fk (a int references d4_primary);
insert into d4_fk values (2);
create table d4_pid (pid int);
}
session "s1"
step "s1b" { begin; }
step "s1brr" { begin isolation level repeatable read; }
step "s1s" { select * from d4_primary; }
step "s1cancel" { select pg_cancel_backend(pid) from d4_pid; }
step "s1insert" { insert into d4_fk values (1); }
step "s1c" { commit; }
step "s1declare" { declare f cursor for select * from d4_primary; }
step "s1declare2" { declare f cursor for select * from d4_fk where a = 2; }
step "s1fetchall" { fetch all from f; }
step "s1fetchone" { fetch 1 from f; }
step "s1updcur" { update d4_fk set a = 1 where current of f; }
step "s1svpt" { savepoint f; }
step "s1rollback" { rollback to f; }
session "s2"
step "s2snitch" { insert into d4_pid select pg_backend_pid(); }
step "s2detach" { alter table d4_primary detach partition d4_primary1 concurrently; }
session "s3"
step "s3brr" { begin isolation level repeatable read; }
step "s3insert" { insert into d4_fk values (1); }
step "s3commit" { commit; }
step "s3vacfreeze" { vacuum freeze pg_catalog.pg_inherits; }
# Trying to insert into a partially detached partition is rejected
permutation "s2snitch" "s1b" "s1s" "s2detach" "s1cancel" "s1insert" "s1c"
permutation "s2snitch" "s1b" "s1s" "s2detach" "s1insert" "s1c"
# ... even under REPEATABLE READ mode.
permutation "s2snitch" "s1brr" "s1s" "s2detach" "s1cancel" "s1insert" "s1c"
permutation "s2snitch" "s1brr" "s1s" "s2detach" "s1insert" "s1c"
# If you read the referenced table using a cursor, you can see a row that the
# RI query does not see.
permutation "s2snitch" "s1b" "s1declare" "s2detach" "s1cancel" "s1fetchall" "s1insert" "s1c"
permutation "s2snitch" "s1b" "s1declare" "s2detach" "s1fetchall" "s1insert" "s1c"
permutation "s2snitch" "s1b" "s1declare" "s2detach" "s1cancel" "s1svpt" "s1insert" "s1rollback" "s1fetchall" "s1c"
permutation "s2snitch" "s1b" "s1declare" "s2detach" "s1svpt" "s1insert" "s1rollback" "s1fetchall" "s1c"
permutation "s2snitch" "s1b" "s2detach" "s1declare" "s1cancel" "s1fetchall" "s1insert" "s1c"
permutation "s2snitch" "s1b" "s2detach" "s1declare" "s1fetchall" "s1insert" "s1c"
permutation "s2snitch" "s1b" "s2detach" "s1declare" "s1cancel" "s1svpt" "s1insert" "s1rollback" "s1fetchall" "s1c"
permutation "s2snitch" "s1b" "s2detach" "s1declare" "s1svpt" "s1insert" "s1rollback" "s1fetchall" "s1c"
# Creating the referencing row using a cursor
permutation "s2snitch" "s1brr" "s1declare2" "s1fetchone" "s2detach" "s1cancel" "s1updcur" "s1c"
permutation "s2snitch" "s1brr" "s1declare2" "s1fetchone" "s2detach" "s1updcur" "s1c"
permutation "s2snitch" "s1brr" "s1declare2" "s1fetchone" "s1updcur" "s2detach" "s1c"
# Try reading the table from an independent session.
permutation "s2snitch" "s1b" "s1s" "s2detach" "s3insert" "s1c"
permutation "s2snitch" "s1b" "s1s" "s2detach" "s3brr" "s3insert" "s3commit" "s1cancel" "s1c"
permutation "s2snitch" "s1b" "s1s" "s2detach" "s3brr" "s3insert" "s3commit" "s1c"
# Try one where we VACUUM FREEZE pg_inherits (to verify that xmin change is
# handled correctly).
permutation "s2snitch" "s1brr" "s1s" "s2detach" "s1cancel" "s3vacfreeze" "s1s" "s1insert" "s1c"
permutation "s2snitch" "s1b" "s1s" "s2detach" "s1cancel" "s3vacfreeze" "s1s" "s1insert" "s1c"
......@@ -7,7 +7,8 @@ OBJS = \
$(WIN32RES) \
delay_execution.o
ISOLATION = partition-addition
ISOLATION = partition-addition \
partition-removal-1
ifdef USE_PGXS
PG_CONFIG = pg_config
......
Parsed test spec with 3 sessions
starting permutation: s3lock s1b s1exec s2remp s3check s3unlock s3check s1c
step s3lock: SELECT pg_advisory_lock(12543);
pg_advisory_lock
step s1b: BEGIN;
step s1exec: SELECT * FROM partrem WHERE a <> 1 AND a <> (SELECT 3); <waiting ...>
step s2remp: ALTER TABLE partrem DETACH PARTITION partrem2 CONCURRENTLY; <waiting ...>
step s3check: SELECT * FROM partrem;
a b
1 ABC
3 DEF
step s3unlock: SELECT pg_advisory_unlock(12543);
pg_advisory_unlock
t
step s1exec: <... completed>
a b
2 JKL
step s3check: SELECT * FROM partrem;
a b
1 ABC
3 DEF
step s1c: COMMIT;
step s2remp: <... completed>
starting permutation: s3lock s1brr s1exec s2remp s3check s3unlock s3check s1c
step s3lock: SELECT pg_advisory_lock(12543);
pg_advisory_lock
step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
step s1exec: SELECT * FROM partrem WHERE a <> 1 AND a <> (SELECT 3); <waiting ...>
step s2remp: ALTER TABLE partrem DETACH PARTITION partrem2 CONCURRENTLY; <waiting ...>
step s3check: SELECT * FROM partrem;
a b
1 ABC
3 DEF
step s3unlock: SELECT pg_advisory_unlock(12543);
pg_advisory_unlock
t
step s1exec: <... completed>
a b
2 JKL
step s3check: SELECT * FROM partrem;
a b
1 ABC
3 DEF
step s1c: COMMIT;
step s2remp: <... completed>
starting permutation: s3lock s1b s1exec2 s2remp s3unlock s1c
step s3lock: SELECT pg_advisory_lock(12543);
pg_advisory_lock
step s1b: BEGIN;
step s1exec2: SELECT * FROM partrem WHERE a <> (SELECT 2) AND a <> 1; <waiting ...>
step s2remp: ALTER TABLE partrem DETACH PARTITION partrem2 CONCURRENTLY; <waiting ...>
step s3unlock: SELECT pg_advisory_unlock(12543);
pg_advisory_unlock
t
step s1exec2: <... completed>
a b
3 DEF
step s1c: COMMIT;
step s2remp: <... completed>
starting permutation: s3lock s1brr s1exec2 s2remp s3unlock s1c
step s3lock: SELECT pg_advisory_lock(12543);
pg_advisory_lock
step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
step s1exec2: SELECT * FROM partrem WHERE a <> (SELECT 2) AND a <> 1; <waiting ...>
step s2remp: ALTER TABLE partrem DETACH PARTITION partrem2 CONCURRENTLY; <waiting ...>
step s3unlock: SELECT pg_advisory_unlock(12543);
pg_advisory_unlock
t
step s1exec2: <... completed>
a b
3 DEF
step s1c: COMMIT;
step s2remp: <... completed>
starting permutation: s3lock s1brr s1prepare s2remp s1execprep s3unlock s1check s1c s1check s1dealloc
step s3lock: SELECT pg_advisory_lock(12543);
pg_advisory_lock
step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
step s1prepare: PREPARE ins AS INSERT INTO partrem VALUES ($1, 'GHI');
step s2remp: ALTER TABLE partrem DETACH PARTITION partrem2 CONCURRENTLY; <waiting ...>
step s1execprep: EXECUTE ins(2); <waiting ...>
step s3unlock: SELECT pg_advisory_unlock(12543);
pg_advisory_unlock
t
step s1execprep: <... completed>
step s1check: SELECT * FROM partrem WHERE b = 'GHI';
a b
2 GHI
step s1c: COMMIT;
step s2remp: <... completed>
step s1check: SELECT * FROM partrem WHERE b = 'GHI';
a b
step s1dealloc: DEALLOCATE ins;
starting permutation: s1brr s1prepare s2remp s3lock s1execprep s3unlock s1check s1c s1check s1dealloc
step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
step s1prepare: PREPARE ins AS INSERT INTO partrem VALUES ($1, 'GHI');
step s2remp: ALTER TABLE partrem DETACH PARTITION partrem2 CONCURRENTLY; <waiting ...>
step s3lock: SELECT pg_advisory_lock(12543);
pg_advisory_lock
step s1execprep: EXECUTE ins(2); <waiting ...>
step s3unlock: SELECT pg_advisory_unlock(12543);
pg_advisory_unlock
t
step s1execprep: <... completed>
step s1check: SELECT * FROM partrem WHERE b = 'GHI';
a b
2 GHI
step s1c: COMMIT;
step s2remp: <... completed>
step s1check: SELECT * FROM partrem WHERE b = 'GHI';
a b
step s1dealloc: DEALLOCATE ins;
starting permutation: s1brr s1check s3lock s2remp s1prepare s1execprep s3unlock s1check s1c s1check s1dealloc
step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ;
step s1check: SELECT * FROM partrem WHERE b = 'GHI';
a b
step s3lock: SELECT pg_advisory_lock(12543);
pg_advisory_lock
step s2remp: ALTER TABLE partrem DETACH PARTITION partrem2 CONCURRENTLY; <waiting ...>
step s1prepare: PREPARE ins AS INSERT INTO partrem VALUES ($1, 'GHI');
step s1execprep: EXECUTE ins(2); <waiting ...>
step s3unlock: SELECT pg_advisory_unlock(12543);
pg_advisory_unlock
t
step s1execprep: <... completed>
step s1check: SELECT * FROM partrem WHERE b = 'GHI';
a b
2 GHI
step s1c: COMMIT;
step s2remp: <... completed>
step s1check: SELECT * FROM partrem WHERE b = 'GHI';
a b
step s1dealloc: DEALLOCATE ins;
# Test removal of a partition with less-than-exclusive locking.
setup
{
CREATE TABLE partrem (a int, b text) PARTITION BY LIST(a);
CREATE TABLE partrem1 PARTITION OF partrem FOR VALUES IN (1);
CREATE TABLE partrem2 PARTITION OF partrem FOR VALUES IN (2);
CREATE TABLE partrem3 PARTITION OF partrem FOR VALUES IN (3);
INSERT INTO partrem VALUES (1, 'ABC');
INSERT INTO partrem VALUES (2, 'JKL');
INSERT INTO partrem VALUES (3, 'DEF');
}
teardown
{
DROP TABLE IF EXISTS partrem, partrem2;
}
session "s1"
setup { LOAD 'delay_execution';
SET delay_execution.post_planning_lock_id = 12543; }
step "s1b" { BEGIN; }
step "s1brr" { BEGIN ISOLATION LEVEL REPEATABLE READ; }
step "s1exec" { SELECT * FROM partrem WHERE a <> 1 AND a <> (SELECT 3); }
step "s1exec2" { SELECT * FROM partrem WHERE a <> (SELECT 2) AND a <> 1; }
step "s1prepare" { PREPARE ins AS INSERT INTO partrem VALUES ($1, 'GHI'); }
step "s1execprep" { EXECUTE ins(2); }
step "s1check" { SELECT * FROM partrem WHERE b = 'GHI'; }
step "s1c" { COMMIT; }
step "s1dealloc" { DEALLOCATE ins; }
session "s2"
step "s2remp" { ALTER TABLE partrem DETACH PARTITION partrem2 CONCURRENTLY; }
session "s3"
step "s3lock" { SELECT pg_advisory_lock(12543); }
step "s3unlock" { SELECT pg_advisory_unlock(12543); }
step "s3check" { SELECT * FROM partrem; }
# The SELECT will be planned with all three partitions shown above,
# of which we expect partrem1 to be pruned at planning and partrem3 at
# execution. Then we'll block, and by the time the query is actually
# executed, detach of partrem2 is already underway (so its row doesn't
# show up in s3's result), but we expect its row to still appear in the
# result for s1.
permutation "s3lock" "s1b" "s1exec" "s2remp" "s3check" "s3unlock" "s3check" "s1c"
permutation "s3lock" "s1brr" "s1exec" "s2remp" "s3check" "s3unlock" "s3check" "s1c"
# In this case we're testing that after pruning partrem2 at runtime, the
# query still works correctly.
permutation "s3lock" "s1b" "s1exec2" "s2remp" "s3unlock" "s1c"
permutation "s3lock" "s1brr" "s1exec2" "s2remp" "s3unlock" "s1c"
# In this case we test that an insert that's prepared in repeatable read
# mode still works after detaching.
permutation "s3lock" "s1brr" "s1prepare" "s2remp" "s1execprep" "s3unlock" "s1check" "s1c" "s1check" "s1dealloc"
permutation "s1brr" "s1prepare" "s2remp" "s3lock" "s1execprep" "s3unlock" "s1check" "s1c" "s1check" "s1dealloc"
permutation "s1brr" "s1check" "s3lock" "s2remp" "s1prepare" "s1execprep" "s3unlock" "s1check" "s1c" "s1check" "s1dealloc"
......@@ -4163,6 +4163,35 @@ SELECT * from part_rp;
(0 rows)
DROP TABLE part_rp;
-- concurrent detach
CREATE TABLE range_parted2 (
a int
) PARTITION BY RANGE(a);
CREATE TABLE part_rp PARTITION OF range_parted2 FOR VALUES FROM (0) to (100);
BEGIN;
-- doesn't work in a partition block
ALTER TABLE range_parted2 DETACH PARTITION part_rp CONCURRENTLY;
ERROR: ALTER TABLE ... DETACH CONCURRENTLY cannot run inside a transaction block
COMMIT;
CREATE TABLE part_rpd PARTITION OF range_parted2 DEFAULT;
-- doesn't work if there's a default partition
ALTER TABLE range_parted2 DETACH PARTITION part_rp CONCURRENTLY;
ERROR: cannot detach partitions concurrently when a default partition exists
-- doesn't work for the default partition
ALTER TABLE range_parted2 DETACH PARTITION part_rpd CONCURRENTLY;
ERROR: cannot detach partitions concurrently when a default partition exists
DROP TABLE part_rpd;
-- works fine
ALTER TABLE range_parted2 DETACH PARTITION part_rp CONCURRENTLY;
\d+ range_parted2
Partitioned table "public.range_parted2"
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
--------+---------+-----------+----------+---------+---------+--------------+-------------
a | integer | | | | plain | |
Partition key: RANGE (a)
Number of partitions: 0
DROP TABLE range_parted2;
-- Check ALTER TABLE commands for partitioned tables and partitions
-- cannot add/drop column to/from *only* the parent
ALTER TABLE ONLY list_parted2 ADD COLUMN c int;
......
......@@ -2678,6 +2678,26 @@ DROP TABLE range_parted2;
SELECT * from part_rp;
DROP TABLE part_rp;
-- concurrent detach
CREATE TABLE range_parted2 (
a int
) PARTITION BY RANGE(a);
CREATE TABLE part_rp PARTITION OF range_parted2 FOR VALUES FROM (0) to (100);
BEGIN;
-- doesn't work in a partition block
ALTER TABLE range_parted2 DETACH PARTITION part_rp CONCURRENTLY;
COMMIT;
CREATE TABLE part_rpd PARTITION OF range_parted2 DEFAULT;
-- doesn't work if there's a default partition
ALTER TABLE range_parted2 DETACH PARTITION part_rp CONCURRENTLY;
-- doesn't work for the default partition
ALTER TABLE range_parted2 DETACH PARTITION part_rpd CONCURRENTLY;
DROP TABLE part_rpd;
-- works fine
ALTER TABLE range_parted2 DETACH PARTITION part_rp CONCURRENTLY;
\d+ range_parted2
DROP TABLE range_parted2;
-- Check ALTER TABLE commands for partitioned tables and partitions
-- cannot add/drop column to/from *only* the parent
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment