ALTER TABLE ... DETACH PARTITION ... CONCURRENTLY
authorAlvaro Herrera <[email protected]>
Thu, 25 Mar 2021 21:00:28 +0000 (18:00 -0300)
committerAlvaro Herrera <[email protected]>
Thu, 25 Mar 2021 21:00:28 +0000 (18:00 -0300)
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 <[email protected]>
Reviewed-by: Amit Langote <[email protected]>
Reviewed-by: Justin Pryzby <[email protected]>
Discussion: https://postgr.es/m/20200803234854[email protected]

41 files changed:
doc/src/sgml/catalogs.sgml
doc/src/sgml/ref/alter_table.sgml
src/backend/catalog/heap.c
src/backend/catalog/index.c
src/backend/catalog/partition.c
src/backend/catalog/pg_inherits.c
src/backend/commands/indexcmds.c
src/backend/commands/tablecmds.c
src/backend/commands/trigger.c
src/backend/executor/execPartition.c
src/backend/nodes/copyfuncs.c
src/backend/nodes/equalfuncs.c
src/backend/optimizer/util/plancat.c
src/backend/parser/gram.y
src/backend/partitioning/partbounds.c
src/backend/partitioning/partdesc.c
src/backend/tcop/utility.c
src/backend/utils/adt/ri_triggers.c
src/backend/utils/cache/partcache.c
src/bin/psql/describe.c
src/include/catalog/catversion.h
src/include/catalog/partition.h
src/include/catalog/pg_inherits.h
src/include/nodes/parsenodes.h
src/include/parser/kwlist.h
src/include/partitioning/partdesc.h
src/include/utils/snapmgr.h
src/test/isolation/expected/detach-partition-concurrently-1.out [new file with mode: 0644]
src/test/isolation/expected/detach-partition-concurrently-2.out [new file with mode: 0644]
src/test/isolation/expected/detach-partition-concurrently-3.out [new file with mode: 0644]
src/test/isolation/expected/detach-partition-concurrently-4.out [new file with mode: 0644]
src/test/isolation/isolation_schedule
src/test/isolation/specs/detach-partition-concurrently-1.spec [new file with mode: 0644]
src/test/isolation/specs/detach-partition-concurrently-2.spec [new file with mode: 0644]
src/test/isolation/specs/detach-partition-concurrently-3.spec [new file with mode: 0644]
src/test/isolation/specs/detach-partition-concurrently-4.spec [new file with mode: 0644]
src/test/modules/delay_execution/Makefile
src/test/modules/delay_execution/expected/partition-removal-1.out [new file with mode: 0644]
src/test/modules/delay_execution/specs/partition-removal-1.spec [new file with mode: 0644]
src/test/regress/expected/alter_table.out
src/test/regress/sql/alter_table.sql

index bae4d8cdd36a9cb294751fc630ac6168348e5c11..cd00d9e3bb0ec57dca8d8b21f215454c7e0ad3b2 100644 (file)
@@ -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>
index cd56d6a17ec5a859727a11671cb814f63e44543e..07e37a6dc81a2812636777fce05925331869b970 100644 (file)
@@ -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>
 
index d0ec44bb40ebe53c96830ad2db86b4bf2e248c98..9f6303266f901752ba0d1dd541f264de897a426f 100644 (file)
@@ -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);
 
index 397d70d2269ff7929e30b42741f658461f3b3ae7..af30840c0447b88f81290511e434ab5dbd5e2cea 100644 (file)
@@ -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
index af7754d6ab76a733832ce1d8a2e0cfc8e02c6212..790f4ccb92772bc744d8d7d042df6544af8285c9 100644 (file)
@@ -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;
index 5ab79028274a769bf80d07667055ca43d5460c6e..bedee069be22493a0ba6b04b80a68f5e8ec284bb 100644 (file)
@@ -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 */
+}
index e4e1bbb7e00bffda25fad94e1fda79493e8a930d..166374cc0c91e6f080bea30910b1d677a06a5604 100644 (file)
@@ -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;
index 550b84681e498b547ece7096133a7aec974cf61c..efac06f72c7465603a684813cd66c618cf5c4336 100644 (file)
@@ -552,7 +552,8 @@ static PartitionSpec *transformPartitionSpec(Relation rel, PartitionSpec *partsp
 static void ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNumber *partattrs,
                                  List **partexprs, Oid *partopclass, Oid *partcollation, char strategy);
 static void CreateInheritance(Relation child_rel, Relation parent_rel);
-static void RemoveInheritance(Relation child_rel, Relation parent_rel);
+static void RemoveInheritance(Relation child_rel, Relation parent_rel,
+                             bool allow_detached);
 static ObjectAddress ATExecAttachPartition(List **wqueue, Relation rel,
                                           PartitionCmd *cmd,
                                           AlterTableUtilityContext *context);
@@ -561,8 +562,14 @@ static void QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
                                               List *partConstraint,
                                               bool validate_default);
 static void CloneRowTriggersToPartition(Relation parent, Relation partition);
+static void DetachAddConstraintIfNeeded(List **wqueue, Relation partRel);
 static void DropClonedTriggersFromPartition(Oid partitionId);
-static ObjectAddress ATExecDetachPartition(Relation rel, RangeVar *name);
+static ObjectAddress ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab,
+                                          Relation rel, RangeVar *name,
+                                          bool concurrent);
+static void DetachPartitionFinalize(Relation rel, Relation partRel,
+                                   bool concurrent, Oid defaultPartOid);
+static ObjectAddress ATExecDetachPartitionFinalize(Relation rel, RangeVar *name);
 static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation rel,
                                              RangeVar *name);
 static void validatePartitionedIndex(Relation partedIdx, Relation partedTbl);
@@ -1010,7 +1017,8 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
         * lock the partition so as to avoid a deadlock.
         */
        defaultPartOid =
-           get_default_oid_from_partdesc(RelationGetPartitionDesc(parent));
+           get_default_oid_from_partdesc(RelationGetPartitionDesc(parent,
+                                                                  false));
        if (OidIsValid(defaultPartOid))
            defaultRel = table_open(defaultPartOid, AccessExclusiveLock);
 
@@ -1563,7 +1571,7 @@ RangeVarCallbackForDropRelation(const RangeVar *rel, Oid relOid, Oid oldRelOid,
     */
    if (is_partition && relOid != oldRelOid)
    {
-       state->partParentOid = get_partition_parent(relOid);
+       state->partParentOid = get_partition_parent(relOid, true);
        if (OidIsValid(state->partParentOid))
            LockRelationOid(state->partParentOid, AccessExclusiveLock);
    }
@@ -3323,7 +3331,7 @@ renameatt_internal(Oid myrelid,
         * expected_parents will only be 0 if we are not already recursing.
         */
        if (expected_parents == 0 &&
-           find_inheritance_children(myrelid, NoLock) != NIL)
+           find_inheritance_children(myrelid, false, NoLock) != NIL)
            ereport(ERROR,
                    (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
                     errmsg("inherited column \"%s\" must be renamed in child tables too",
@@ -3522,7 +3530,7 @@ rename_constraint_internal(Oid myrelid,
        else
        {
            if (expected_parents == 0 &&
-               find_inheritance_children(myrelid, NoLock) != NIL)
+               find_inheritance_children(myrelid, false, NoLock) != NIL)
                ereport(ERROR,
                        (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
                         errmsg("inherited constraint \"%s\" must be renamed in child tables too",
@@ -4142,7 +4150,14 @@ AlterTableGetLockLevel(List *cmds)
                break;
 
            case AT_DetachPartition:
-               cmd_lockmode = AccessExclusiveLock;
+               if (((PartitionCmd *) cmd->def)->concurrent)
+                   cmd_lockmode = ShareUpdateExclusiveLock;
+               else
+                   cmd_lockmode = AccessExclusiveLock;
+               break;
+
+           case AT_DetachPartitionFinalize:
+               cmd_lockmode = ShareUpdateExclusiveLock;
                break;
 
            case AT_CheckNotNull:
@@ -4226,6 +4241,19 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
    /* Find or create work queue entry for this table */
    tab = ATGetQueueEntry(wqueue, rel);
 
+   /*
+    * Disallow any ALTER TABLE other than ALTER TABLE DETACH FINALIZE on
+    * partitions that are pending detach.
+    */
+   if (rel->rd_rel->relispartition &&
+       cmd->subtype != AT_DetachPartitionFinalize &&
+       PartitionHasPendingDetach(RelationGetRelid(rel)))
+       ereport(ERROR,
+               errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+               errmsg("cannot alter partition \"%s\" with an incomplete detach",
+                      RelationGetRelationName(rel)),
+               errhint("Use ALTER TABLE ... DETACH PARTITION ... FINALIZE to complete the pending detach operation."));
+
    /*
     * Copy the original subcommand for each table.  This avoids conflicts
     * when different child tables need to make different parse
@@ -4539,6 +4567,11 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
            /* No command-specific prep needed */
            pass = AT_PASS_MISC;
            break;
+       case AT_DetachPartitionFinalize:
+           ATSimplePermissions(rel, ATT_TABLE);
+           /* No command-specific prep needed */
+           pass = AT_PASS_MISC;
+           break;
        default:                /* oops */
            elog(ERROR, "unrecognized alter table type: %d",
                 (int) cmd->subtype);
@@ -4930,7 +4963,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
            Assert(cmd != NULL);
            /* ATPrepCmd ensures it must be a table */
            Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
-           ATExecDetachPartition(rel, ((PartitionCmd *) cmd->def)->name);
+           ATExecDetachPartition(wqueue, tab, rel,
+                                 ((PartitionCmd *) cmd->def)->name,
+                                 ((PartitionCmd *) cmd->def)->concurrent);
+           break;
+       case AT_DetachPartitionFinalize:
+           ATExecDetachPartitionFinalize(rel, ((PartitionCmd *) cmd->def)->name);
            break;
        case AT_AlterCollationRefreshVersion:
            /* ATPrepCmd ensured it must be an index */
@@ -6362,7 +6400,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
     */
    if (colDef->identity &&
        recurse &&
-       find_inheritance_children(myrelid, NoLock) != NIL)
+       find_inheritance_children(myrelid, false, NoLock) != NIL)
        ereport(ERROR,
                (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
                 errmsg("cannot recursively add identity column to table that has child tables")));
@@ -6607,7 +6645,8 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
     * routines, we have to do this one level of recursion at a time; we can't
     * use find_all_inheritors to do it in one pass.
     */
-   children = find_inheritance_children(RelationGetRelid(rel), lockmode);
+   children =
+       find_inheritance_children(RelationGetRelid(rel), false, lockmode);
 
    /*
     * If we are told not to recurse, there had better not be any child
@@ -6761,7 +6800,7 @@ ATPrepDropNotNull(Relation rel, bool recurse, bool recursing)
     */
    if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
    {
-       PartitionDesc partdesc = RelationGetPartitionDesc(rel);
+       PartitionDesc partdesc = RelationGetPartitionDesc(rel, false);
 
        Assert(partdesc != NULL);
        if (partdesc->nparts > 0 && !recurse && !recursing)
@@ -6860,7 +6899,7 @@ ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode)
    /* If rel is partition, shouldn't drop NOT NULL if parent has the same */
    if (rel->rd_rel->relispartition)
    {
-       Oid         parentId = get_partition_parent(RelationGetRelid(rel));
+       Oid         parentId = get_partition_parent(RelationGetRelid(rel), false);
        Relation    parent = table_open(parentId, AccessShareLock);
        TupleDesc   tupDesc = RelationGetDescr(parent);
        AttrNumber  parent_attnum;
@@ -7470,7 +7509,7 @@ ATPrepDropExpression(Relation rel, AlterTableCmd *cmd, bool recurse, bool recurs
     * resulting state can be properly dumped and restored.
     */
    if (!recurse &&
-       find_inheritance_children(RelationGetRelid(rel), lockmode))
+       find_inheritance_children(RelationGetRelid(rel), false, lockmode))
        ereport(ERROR,
                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                 errmsg("ALTER TABLE / DROP EXPRESSION must be applied to child tables too")));
@@ -8077,7 +8116,8 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
     * routines, we have to do this one level of recursion at a time; we can't
     * use find_all_inheritors to do it in one pass.
     */
-   children = find_inheritance_children(RelationGetRelid(rel), lockmode);
+   children =
+       find_inheritance_children(RelationGetRelid(rel), false, lockmode);
 
    if (children)
    {
@@ -8541,7 +8581,8 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
     * routines, we have to do this one level of recursion at a time; we can't
     * use find_all_inheritors to do it in one pass.
     */
-   children = find_inheritance_children(RelationGetRelid(rel), lockmode);
+   children =
+       find_inheritance_children(RelationGetRelid(rel), false, lockmode);
 
    /*
     * Check if ONLY was specified with ALTER TABLE.  If so, allow the
@@ -9156,7 +9197,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
     */
    if (pkrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
    {
-       PartitionDesc pd = RelationGetPartitionDesc(pkrel);
+       PartitionDesc pd = RelationGetPartitionDesc(pkrel, false);
 
        for (int i = 0; i < pd->nparts; i++)
        {
@@ -9290,7 +9331,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
    }
    else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
    {
-       PartitionDesc pd = RelationGetPartitionDesc(rel);
+       PartitionDesc pd = RelationGetPartitionDesc(rel, false);
 
        /*
         * Recurse to take appropriate action on each partition; either we
@@ -11074,7 +11115,8 @@ ATExecDropConstraint(Relation rel, const char *constrName,
     * use find_all_inheritors to do it in one pass.
     */
    if (!is_no_inherit_constraint)
-       children = find_inheritance_children(RelationGetRelid(rel), lockmode);
+       children =
+           find_inheritance_children(RelationGetRelid(rel), false, lockmode);
    else
        children = NIL;
 
@@ -11458,7 +11500,8 @@ ATPrepAlterColumnType(List **wqueue,
        }
    }
    else if (!recursing &&
-            find_inheritance_children(RelationGetRelid(rel), NoLock) != NIL)
+            find_inheritance_children(RelationGetRelid(rel), false,
+                                      NoLock) != NIL)
        ereport(ERROR,
                (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
                 errmsg("type of inherited column \"%s\" must be changed in child tables too",
@@ -14296,7 +14339,7 @@ ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode)
     */
 
    /* Off to RemoveInheritance() where most of the work happens */
-   RemoveInheritance(rel, parent_rel);
+   RemoveInheritance(rel, parent_rel, false);
 
    ObjectAddressSet(address, RelationRelationId,
                     RelationGetRelid(parent_rel));
@@ -14307,12 +14350,72 @@ ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode)
    return address;
 }
 
+/*
+ * MarkInheritDetached
+ *
+ * Set inhdetachpending for a partition, for ATExecDetachPartition
+ * in concurrent mode.
+ */
+static void
+MarkInheritDetached(Relation child_rel, Relation parent_rel)
+{
+   Relation    catalogRelation;
+   SysScanDesc scan;
+   ScanKeyData key;
+   HeapTuple   inheritsTuple;
+   bool        found = false;
+
+   Assert(child_rel->rd_rel->relkind == RELKIND_RELATION ||
+          child_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
+   Assert(parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
+
+   /*
+    * Find pg_inherits entries by inhrelid.
+    */
+   catalogRelation = table_open(InheritsRelationId, RowExclusiveLock);
+   ScanKeyInit(&key,
+               Anum_pg_inherits_inhrelid,
+               BTEqualStrategyNumber, F_OIDEQ,
+               ObjectIdGetDatum(RelationGetRelid(child_rel)));
+   scan = systable_beginscan(catalogRelation, InheritsRelidSeqnoIndexId,
+                             true, NULL, 1, &key);
+
+   while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan)))
+   {
+       HeapTuple   newtup;
+
+       if (((Form_pg_inherits) GETSTRUCT(inheritsTuple))->inhparent !=
+           RelationGetRelid(parent_rel))
+           elog(ERROR, "bad parent tuple found for partition %u",
+                RelationGetRelid(child_rel));
+
+       newtup = heap_copytuple(inheritsTuple);
+       ((Form_pg_inherits) GETSTRUCT(newtup))->inhdetachpending = true;
+
+       CatalogTupleUpdate(catalogRelation,
+                          &inheritsTuple->t_self,
+                          newtup);
+       found = true;
+   }
+
+   /* Done */
+   systable_endscan(scan);
+   table_close(catalogRelation, RowExclusiveLock);
+
+   if (!found)
+       ereport(ERROR,
+               (errcode(ERRCODE_UNDEFINED_TABLE),
+                errmsg("relation \"%s\" is not a partition of relation \"%s\"",
+                       RelationGetRelationName(child_rel),
+                       RelationGetRelationName(parent_rel))));
+}
+
 /*
  * RemoveInheritance
  *
  * Drop a parent from the child's parents. This just adjusts the attinhcount
  * and attislocal of the columns and removes the pg_inherit and pg_depend
- * entries.
+ * entries.  expect_detached is passed down to DeleteInheritsTuple, q.v..
  *
  * If attinhcount goes to 0 then attislocal gets set to true. If it goes back
  * up attislocal stays true, which means if a child is ever removed from a
@@ -14326,7 +14429,7 @@ ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode)
  * Common to ATExecDropInherit() and ATExecDetachPartition().
  */
 static void
-RemoveInheritance(Relation child_rel, Relation parent_rel)
+RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached)
 {
    Relation    catalogRelation;
    SysScanDesc scan;
@@ -14342,7 +14445,9 @@ RemoveInheritance(Relation child_rel, Relation parent_rel)
        child_is_partition = true;
 
    found = DeleteInheritsTuple(RelationGetRelid(child_rel),
-                               RelationGetRelid(parent_rel));
+                               RelationGetRelid(parent_rel),
+                               expect_detached,
+                               RelationGetRelationName(child_rel));
    if (!found)
    {
        if (child_is_partition)
@@ -16508,7 +16613,7 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
    }
    else if (scanrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
    {
-       PartitionDesc partdesc = RelationGetPartitionDesc(scanrel);
+       PartitionDesc partdesc = RelationGetPartitionDesc(scanrel, false);
        int         i;
 
        for (i = 0; i < partdesc->nparts; i++)
@@ -16568,7 +16673,7 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
     * new partition will change its partition constraint.
     */
    defaultPartOid =
-       get_default_oid_from_partdesc(RelationGetPartitionDesc(rel));
+       get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, false));
    if (OidIsValid(defaultPartOid))
        LockRelationOid(defaultPartOid, AccessExclusiveLock);
 
@@ -17157,105 +17262,213 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
  * ALTER TABLE DETACH PARTITION
  *
  * Return the address of the relation that is no longer a partition of rel.
+ *
+ * If concurrent mode is requested, we run in two transactions.  A side-
+ * effect is that this command cannot run in a multi-part ALTER TABLE.
+ * Currently, that's enforced by the grammar.
+ *
+ * The strategy for concurrency is to first modify the partition's
+ * pg_inherit catalog row to make it visible to everyone that the
+ * partition is detached, lock the partition against writes, and commit
+ * the transaction; anyone who requests the partition descriptor from
+ * that point onwards has to ignore such a partition.  In a second
+ * transaction, we wait until all transactions that could have seen the
+ * partition as attached are gone, then we remove the rest of partition
+ * metadata (pg_inherits and pg_class.relpartbounds).
  */
 static ObjectAddress
-ATExecDetachPartition(Relation rel, RangeVar *name)
+ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
+                     RangeVar *name, bool concurrent)
 {
-   Relation    partRel,
-               classRel;
-   HeapTuple   tuple,
-               newtuple;
-   Datum       new_val[Natts_pg_class];
-   bool        new_null[Natts_pg_class],
-               new_repl[Natts_pg_class];
+   Relation    partRel;
    ObjectAddress address;
    Oid         defaultPartOid;
-   List       *indexes;
-   List       *fks;
-   ListCell   *cell;
 
    /*
     * We must lock the default partition, because detaching this partition
     * will change its partition constraint.
     */
    defaultPartOid =
-       get_default_oid_from_partdesc(RelationGetPartitionDesc(rel));
+       get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, false));
    if (OidIsValid(defaultPartOid))
+   {
+       /*
+        * Concurrent detaching when a default partition exists is not
+        * supported. The main problem is that the default partition
+        * constraint would change.  And there's a definitional problem: what
+        * should happen to the tuples that are being inserted that belong to
+        * the partition being detached?  Putting them on the partition being
+        * detached would be wrong, since they'd become "lost" after the but
+        * we cannot put them in the default partition either until we alter
+        * its partition constraint.
+        *
+        * I think we could solve this problem if we effected the constraint
+        * change before committing the first transaction.  But the lock would
+        * have to remain AEL and it would cause concurrent query planning to
+        * be blocked, so changing it that way would be even worse.
+        */
+       if (concurrent)
+           ereport(ERROR,
+                   (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+                    errmsg("cannot detach partitions concurrently when a default partition exists")));
        LockRelationOid(defaultPartOid, AccessExclusiveLock);
+   }
 
-   partRel = table_openrv(name, ShareUpdateExclusiveLock);
+   /*
+    * In concurrent mode, the partition is locked with share-update-exclusive
+    * in the first transaction.  This allows concurrent transactions to be
+    * doing DML to the partition.
+    */
+   partRel = table_openrv(name, concurrent ? ShareUpdateExclusiveLock :
+                          AccessExclusiveLock);
 
-   /* Ensure that foreign keys still hold after this detach */
+   /*
+    * Check inheritance conditions and either delete the pg_inherits row
+    * (in non-concurrent mode) or just set the inhdetachpending flag.
+    */
+   if (!concurrent)
+       RemoveInheritance(partRel, rel, false);
+   else
+       MarkInheritDetached(partRel, rel);
+
+   /*
+    * Ensure that foreign keys still hold after this detach.  This keeps
+    * locks on the referencing tables, which prevents concurrent transactions
+    * from adding rows that we wouldn't see.  For this to work in concurrent
+    * mode, it is critical that the partition appears as no longer attached
+    * for the RI queries as soon as the first transaction commits.
+    */
    ATDetachCheckNoForeignKeyRefs(partRel);
 
-   /* All inheritance related checks are performed within the function */
-   RemoveInheritance(partRel, rel);
+   /*
+    * Concurrent mode has to work harder; first we add a new constraint to
+    * the partition that matches the partition constraint.  Then we close our
+    * existing transaction, and in a new one wait for all processes to catch
+    * up on the catalog updates we've done so far; at that point we can
+    * complete the operation.
+    */
+   if (concurrent)
+   {
+       Oid     partrelid,
+               parentrelid;
+       LOCKTAG     tag;
+       char   *parentrelname;
+       char   *partrelname;
 
-   /* Update pg_class tuple */
-   classRel = table_open(RelationRelationId, RowExclusiveLock);
-   tuple = SearchSysCacheCopy1(RELOID,
-                               ObjectIdGetDatum(RelationGetRelid(partRel)));
-   if (!HeapTupleIsValid(tuple))
-       elog(ERROR, "cache lookup failed for relation %u",
-            RelationGetRelid(partRel));
-   Assert(((Form_pg_class) GETSTRUCT(tuple))->relispartition);
+       /*
+        * Add a new constraint to the partition being detached, which
+        * supplants the partition constraint (unless there is one already).
+        */
+       DetachAddConstraintIfNeeded(wqueue, partRel);
 
-   /* Clear relpartbound and reset relispartition */
-   memset(new_val, 0, sizeof(new_val));
-   memset(new_null, false, sizeof(new_null));
-   memset(new_repl, false, sizeof(new_repl));
-   new_val[Anum_pg_class_relpartbound - 1] = (Datum) 0;
-   new_null[Anum_pg_class_relpartbound - 1] = true;
-   new_repl[Anum_pg_class_relpartbound - 1] = true;
-   newtuple = heap_modify_tuple(tuple, RelationGetDescr(classRel),
-                                new_val, new_null, new_repl);
+       /*
+        * We're almost done now; the only traces that remain are the
+        * pg_inherits tuple and the partition's relpartbounds.  Before we can
+        * remove those, we need to wait until all transactions that know that
+        * this is a partition are gone.
+        */
 
-   ((Form_pg_class) GETSTRUCT(newtuple))->relispartition = false;
-   CatalogTupleUpdate(classRel, &newtuple->t_self, newtuple);
-   heap_freetuple(newtuple);
+       /*
+        * Remember relation OIDs to re-acquire them later; and relation names
+        * too, for error messages if something is dropped in between.
+        */
+       partrelid = RelationGetRelid(partRel);
+       parentrelid = RelationGetRelid(rel);
+       parentrelname = MemoryContextStrdup(PortalContext,
+                                           RelationGetRelationName(rel));
+       partrelname = MemoryContextStrdup(PortalContext,
+                                         RelationGetRelationName(partRel));
+
+       /* Invalidate relcache entries for the parent -- must be before close */
+       CacheInvalidateRelcache(rel);
+
+       table_close(partRel, NoLock);
+       table_close(rel, NoLock);
+       tab->rel = NULL;
+
+       /* Make updated catalog entry visible */
+       PopActiveSnapshot();
+       CommitTransactionCommand();
+
+       StartTransactionCommand();
 
-   if (OidIsValid(defaultPartOid))
-   {
        /*
-        * If the relation being detached is the default partition itself,
-        * remove it from the parent's pg_partitioned_table entry.
+        * Now wait.  This ensures that all queries that were planned including
+        * the partition are finished before we remove the rest of catalog
+        * entries.  We don't need or indeed want to acquire this lock, though
+        * -- that would block later queries.
         *
-        * If not, we must invalidate default partition's relcache entry, as
-        * in StorePartitionBound: its partition constraint depends on every
-        * other partition's partition constraint.
+        * We don't need to concern ourselves with waiting for a lock on the
+        * partition itself, since we will acquire AccessExclusiveLock below.
         */
-       if (RelationGetRelid(partRel) == defaultPartOid)
-           update_default_partition_oid(RelationGetRelid(rel), InvalidOid);
-       else
-           CacheInvalidateRelcacheByRelid(defaultPartOid);
+       SET_LOCKTAG_RELATION(tag, MyDatabaseId, parentrelid);
+       WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, false);
+
+       /*
+        * Now acquire locks in both relations again.  Note they may have been
+        * removed in the meantime, so care is required.
+        */
+       rel = try_relation_open(parentrelid, ShareUpdateExclusiveLock);
+       partRel = try_relation_open(partrelid, AccessExclusiveLock);
+
+       /* If the relations aren't there, something bad happened; bail out */
+       if (rel == NULL)
+       {
+           if (partRel != NULL)    /* shouldn't happen */
+               elog(WARNING, "dangling partition \"%s\" remains, can't fix",
+                    partrelname);
+           ereport(ERROR,
+                   (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+                    errmsg("partitioned table \"%s\" was removed concurrently",
+                           parentrelname)));
+       }
+       if (partRel == NULL)
+           ereport(ERROR,
+                   (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+                    errmsg("partition \"%s\" was removed concurrently", partrelname)));
+
+       tab->rel = rel;
    }
 
-   /* detach indexes too */
-   indexes = RelationGetIndexList(partRel);
-   foreach(cell, indexes)
-   {
-       Oid         idxid = lfirst_oid(cell);
-       Relation    idx;
-       Oid         constrOid;
+   /* Do the final part of detaching */
+   DetachPartitionFinalize(rel, partRel, concurrent, defaultPartOid);
 
-       if (!has_superclass(idxid))
-           continue;
+   ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
 
-       Assert((IndexGetRelation(get_partition_parent(idxid), false) ==
-               RelationGetRelid(rel)));
+   /* keep our lock until commit */
+   table_close(partRel, NoLock);
 
-       idx = index_open(idxid, AccessExclusiveLock);
-       IndexSetParentIndex(idx, InvalidOid);
+   return address;
+}
 
-       /* If there's a constraint associated with the index, detach it too */
-       constrOid = get_relation_idx_constraint_oid(RelationGetRelid(partRel),
-                                                   idxid);
-       if (OidIsValid(constrOid))
-           ConstraintSetParentConstraint(constrOid, InvalidOid, InvalidOid);
+/*
+ * Second part of ALTER TABLE .. DETACH.
+ *
+ * This is separate so that it can be run independently when the second
+ * transaction of the concurrent algorithm fails (crash or abort).
+ */
+static void
+DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
+                       Oid defaultPartOid)
+{
+   Relation    classRel;
+   List       *fks;
+   ListCell   *cell;
+   List       *indexes;
+   Datum       new_val[Natts_pg_class];
+   bool        new_null[Natts_pg_class],
+               new_repl[Natts_pg_class];
+   HeapTuple   tuple,
+               newtuple;
 
-       index_close(idx, NoLock);
+   if (concurrent)
+   {
+       /*
+        * We can remove the pg_inherits row now. (In the non-concurrent case,
+        * this was already done).
+        */
+       RemoveInheritance(partRel, rel, true);
    }
-   table_close(classRel, RowExclusiveLock);
 
    /* Drop any triggers that were cloned on creation/attach. */
    DropClonedTriggersFromPartition(RelationGetRelid(partRel));
@@ -17328,22 +17541,161 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
        ObjectAddressSet(constraint, ConstraintRelationId, constrOid);
        performDeletion(&constraint, DROP_RESTRICT, 0);
    }
-   CommandCounterIncrement();
+
+   /* Now we can detach indexes */
+   indexes = RelationGetIndexList(partRel);
+   foreach(cell, indexes)
+   {
+       Oid         idxid = lfirst_oid(cell);
+       Relation    idx;
+       Oid         constrOid;
+
+       if (!has_superclass(idxid))
+           continue;
+
+       Assert((IndexGetRelation(get_partition_parent(idxid, false), false) ==
+               RelationGetRelid(rel)));
+
+       idx = index_open(idxid, AccessExclusiveLock);
+       IndexSetParentIndex(idx, InvalidOid);
+
+       /* If there's a constraint associated with the index, detach it too */
+       constrOid = get_relation_idx_constraint_oid(RelationGetRelid(partRel),
+                                                   idxid);
+       if (OidIsValid(constrOid))
+           ConstraintSetParentConstraint(constrOid, InvalidOid, InvalidOid);
+
+       index_close(idx, NoLock);
+   }
+
+   /* Update pg_class tuple */
+   classRel = table_open(RelationRelationId, RowExclusiveLock);
+   tuple = SearchSysCacheCopy1(RELOID,
+                               ObjectIdGetDatum(RelationGetRelid(partRel)));
+   if (!HeapTupleIsValid(tuple))
+       elog(ERROR, "cache lookup failed for relation %u",
+            RelationGetRelid(partRel));
+   Assert(((Form_pg_class) GETSTRUCT(tuple))->relispartition);
+
+   /* Clear relpartbound and reset relispartition */
+   memset(new_val, 0, sizeof(new_val));
+   memset(new_null, false, sizeof(new_null));
+   memset(new_repl, false, sizeof(new_repl));
+   new_val[Anum_pg_class_relpartbound - 1] = (Datum) 0;
+   new_null[Anum_pg_class_relpartbound - 1] = true;
+   new_repl[Anum_pg_class_relpartbound - 1] = true;
+   newtuple = heap_modify_tuple(tuple, RelationGetDescr(classRel),
+                                new_val, new_null, new_repl);
+
+   ((Form_pg_class) GETSTRUCT(newtuple))->relispartition = false;
+   CatalogTupleUpdate(classRel, &newtuple->t_self, newtuple);
+   heap_freetuple(newtuple);
+   table_close(classRel, RowExclusiveLock);
+
+   if (OidIsValid(defaultPartOid))
+   {
+       /*
+        * If the relation being detached is the default partition itself,
+        * remove it from the parent's pg_partitioned_table entry.
+        *
+        * If not, we must invalidate default partition's relcache entry, as
+        * in StorePartitionBound: its partition constraint depends on every
+        * other partition's partition constraint.
+        */
+       if (RelationGetRelid(partRel) == defaultPartOid)
+           update_default_partition_oid(RelationGetRelid(rel), InvalidOid);
+       else
+           CacheInvalidateRelcacheByRelid(defaultPartOid);
+   }
 
    /*
     * Invalidate the parent's relcache so that the partition is no longer
     * included in its partition descriptor.
     */
    CacheInvalidateRelcache(rel);
+}
+
+/*
+ * ALTER TABLE ... DETACH PARTITION ... FINALIZE
+ *
+ * To use when a DETACH PARTITION command previously did not run to
+ * completion; this completes the detaching process.
+ */
+static ObjectAddress
+ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
+{
+   Relation    partRel;
+   ObjectAddress address;
+   Snapshot    snap = GetActiveSnapshot();
+
+   partRel = table_openrv(name, AccessExclusiveLock);
+
+   /*
+    * Wait until existing snapshots are gone.  This is important if the
+    * second transaction of DETACH PARTITION CONCURRENTLY is canceled: the
+    * user could immediately run DETACH FINALIZE without actually waiting for
+    * existing transactions.  We must not complete the detach action until
+    * all such queries are complete (otherwise we would present them with an
+    * inconsistent view of catalogs).
+    */
+   WaitForOlderSnapshots(snap->xmin, false);
+
+   DetachPartitionFinalize(rel, partRel, true, InvalidOid);
 
    ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
 
-   /* keep our lock until commit */
    table_close(partRel, NoLock);
 
    return address;
 }
 
+/*
+ * DetachAddConstraintIfNeeded
+ *     Subroutine for ATExecDetachPartition.  Create a constraint that
+ *     takes the place of the partition constraint, but avoid creating
+ *     a dupe if an equivalent constraint already exists.
+ */
+static void
+DetachAddConstraintIfNeeded(List **wqueue, Relation partRel)
+{
+   AlteredTableInfo *tab;
+   Expr       *constraintExpr;
+   TupleDesc   td = RelationGetDescr(partRel);
+   Constraint *n;
+
+   constraintExpr = make_ands_explicit(RelationGetPartitionQual(partRel));
+
+   /* If an identical constraint exists, we don't need to create one */
+   if (td->constr && td->constr->num_check > 0)
+   {
+       for (int i = 0; i < td->constr->num_check; i++)
+       {
+           Node    *thisconstr;
+
+           thisconstr = stringToNode(td->constr->check[i].ccbin);
+
+           if (equal(constraintExpr, thisconstr))
+               return;
+       }
+   }
+
+   tab = ATGetQueueEntry(wqueue, partRel);
+
+   /* Add constraint on partition, equivalent to the partition constraint */
+   n = makeNode(Constraint);
+   n->contype = CONSTR_CHECK;
+   n->conname = NULL;
+   n->location = -1;
+   n->is_no_inherit = false;
+   n->raw_expr = NULL;
+   n->cooked_expr = nodeToString(constraintExpr);
+   n->initially_valid = true;
+   n->skip_validation = true;
+   /* It's a re-add, since it nominally already exists */
+   ATAddCheckConstraint(wqueue, tab, partRel, n,
+                        true, false, true, ShareUpdateExclusiveLock);
+}
+
 /*
  * DropClonedTriggersFromPartition
  *     subroutine for ATExecDetachPartition to remove any triggers that were
@@ -17511,7 +17863,7 @@ ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, RangeVar *name)
 
    /* Silently do nothing if already in the right state */
    currParent = partIdx->rd_rel->relispartition ?
-       get_partition_parent(partIdxId) : InvalidOid;
+       get_partition_parent(partIdxId, false) : InvalidOid;
    if (currParent != RelationGetRelid(parentIdx))
    {
        IndexInfo  *childInfo;
@@ -17539,7 +17891,7 @@ ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, RangeVar *name)
                               RelationGetRelationName(partIdx))));
 
        /* Make sure it indexes a partition of the other index's table */
-       partDesc = RelationGetPartitionDesc(parentTbl);
+       partDesc = RelationGetPartitionDesc(parentTbl, false);
        found = false;
        for (i = 0; i < partDesc->nparts; i++)
        {
@@ -17693,7 +18045,7 @@ validatePartitionedIndex(Relation partedIdx, Relation partedTbl)
     * If we found as many inherited indexes as the partitioned table has
     * partitions, we're good; update pg_index to set indisvalid.
     */
-   if (tuples == RelationGetPartitionDesc(partedTbl)->nparts)
+   if (tuples == RelationGetPartitionDesc(partedTbl, false)->nparts)
    {
        Relation    idxRel;
        HeapTuple   newtup;
@@ -17723,8 +18075,8 @@ validatePartitionedIndex(Relation partedIdx, Relation partedTbl)
        /* make sure we see the validation we just did */
        CommandCounterIncrement();
 
-       parentIdxId = get_partition_parent(RelationGetRelid(partedIdx));
-       parentTblId = get_partition_parent(RelationGetRelid(partedTbl));
+       parentIdxId = get_partition_parent(RelationGetRelid(partedIdx), false);
+       parentTblId = get_partition_parent(RelationGetRelid(partedTbl), false);
        parentIdx = relation_open(parentIdxId, AccessExclusiveLock);
        parentTbl = relation_open(parentTblId, AccessExclusiveLock);
        Assert(!parentIdx->rd_index->indisvalid);
index 4e4e05844c5b00339a0a7656e3dbdff694ed2c1f..7383d5994ebedc2d831fc1da6e14a004e8aa2e7d 100644 (file)
@@ -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),
index b8da4c5967d8fc7cd63fb4550d50304f12dc2c43..619aaffae43e8148b9b15a128806e64f180a2fb2 100644 (file)
@@ -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);
index 82d7cce5d57f3b6278e80e263bab43f80b73ff05..38b56231b7df2fb0890b17e5026d295578b5fbe5 100644 (file)
@@ -4737,6 +4737,7 @@ _copyPartitionCmd(const PartitionCmd *from)
 
    COPY_NODE_FIELD(name);
    COPY_NODE_FIELD(bound);
+   COPY_SCALAR_FIELD(concurrent);
 
    return newnode;
 }
index 3e980c457c58ac87b58415120c04d4b24621bb78..3292dda34245084dc2b5e7a4fe7f9d8d713f6410 100644 (file)
@@ -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;
 }
index 7f2e40ae39e9f7c0e84c8baa2e0d4367fbc61637..6c39bf893f886a92eaa1a58a79f16e91fdaf2506 100644 (file)
@@ -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);
index 4843ff99e2d0ab77979fd820cde2206061594c48..2132cf4d828c6252351dab6abac6d2e94dee6565 100644 (file)
@@ -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
index e5f3482d52eca08e00b9cbc1ce4e14c60bc2958a..2b2b1dc1ad75011153e9c2749002f9575e4a3771 100644 (file)
@@ -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;
index f852b6e99de8665afd825cdc71f62fea6fad714c..58570fecfdc3d298248eb3e3ca43b6b3f1471ef4 100644 (file)
@@ -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;
index 05bb698cf456bfe6fab62e1791a569df67394d2a..729274b330f0f1427ec62548f51571bec46be035 100644 (file)
@@ -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
index 09a2ad28814d94a26f0e98f88173cf439a8cdb07..7c77c338cecbc3ea235d4db096bfb1f7521cfac3 100644 (file)
@@ -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)
index a6388d980ed06fffb9790b51b5570d6c2863eefc..21e60f0c5e817836c61c5025c129149986e06596 100644 (file)
@@ -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));
index eeac0efc4fdc8afca7779ac1b4bf83caa678701a..c9f7118a5dcc14798cba7cf91d87f12b391a244a 100644 (file)
@@ -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, ',');
 
index 3c67908f8525da780cf20842d06e3219f7a1e196..5919a0b30526ed05997d63a7d0b717f07892147a 100644 (file)
@@ -53,6 +53,6 @@
  */
 
 /*                         yyyymmddN */
-#define CATALOG_VERSION_NO 202103242
+#define CATALOG_VERSION_NO 202103251
 
 #endif
index fe3f66befaf062e1db8324647dc8c2d6f7fe2685..c8c7bc1d998e3046731b076511c2399122af798a 100644 (file)
@@ -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,
index 2b71cad9a2a48d9f5ed07190f4e946367a0d8c78..6d07e1b302bbc34d919d1f683fe837bc09467b9c 100644 (file)
@@ -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 */
index 68425eb2c0402b742b13e6778bfeb6644ff76ab9..0ce19d98ec8147c5239b81bfa7e2fa698b2ebf3f 100644 (file)
@@ -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 */
index ca1f950cbed44ac1a62e1d3d759b22aaddcef062..4bbe53e8522932bc9166aa0819f8a438ebe7e8d5 100644 (file)
@@ -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)
index ff113199e5c3a02639cfd82943b392d397076dd9..7f03ff427122940f472c5f9978c7fb1a91aeccdf 100644 (file)
@@ -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);
 
index f66ac5818859460bbc1a40fd11f2893cfe47e90c..44539fe15ab4aa94bf3d742bb62ac96bc1c3fb14 100644 (file)
@@ -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,
diff --git a/src/test/isolation/expected/detach-partition-concurrently-1.out b/src/test/isolation/expected/detach-partition-concurrently-1.out
new file mode 100644 (file)
index 0000000..9b45267
--- /dev/null
@@ -0,0 +1,234 @@
+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;
diff --git a/src/test/isolation/expected/detach-partition-concurrently-2.out b/src/test/isolation/expected/detach-partition-concurrently-2.out
new file mode 100644 (file)
index 0000000..85be707
--- /dev/null
@@ -0,0 +1,66 @@
+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;
diff --git a/src/test/isolation/expected/detach-partition-concurrently-3.out b/src/test/isolation/expected/detach-partition-concurrently-3.out
new file mode 100644 (file)
index 0000000..88e8363
--- /dev/null
@@ -0,0 +1,282 @@
+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
diff --git a/src/test/isolation/expected/detach-partition-concurrently-4.out b/src/test/isolation/expected/detach-partition-concurrently-4.out
new file mode 100644 (file)
index 0000000..90a75cb
--- /dev/null
@@ -0,0 +1,351 @@
+Parsed test spec with 3 sessions
+
+starting permutation: s2snitch s1b s1s s2detach s1cancel s1insert s1c
+step s2snitch: insert into d4_pid select pg_backend_pid();
+step s1b: begin;
+step s1s: select * from d4_primary;
+a              
+
+1              
+2              
+step s2detach: alter table d4_primary detach partition d4_primary1 concurrently; <waiting ...>
+step s1cancel: select pg_cancel_backend(pid) from d4_pid;
+pg_cancel_backend
+
+t              
+step s2detach: <... completed>
+error in steps s1cancel s2detach: ERROR:  canceling statement due to user request
+step s1insert: insert into d4_fk values (1);
+ERROR:  insert or update on table "d4_fk" violates foreign key constraint "d4_fk_a_fkey"
+step s1c: commit;
+
+starting permutation: s2snitch s1b s1s s2detach s1insert s1c
+step s2snitch: insert into d4_pid select pg_backend_pid();
+step s1b: begin;
+step s1s: select * from d4_primary;
+a              
+
+1              
+2              
+step s2detach: alter table d4_primary detach partition d4_primary1 concurrently; <waiting ...>
+step s1insert: insert into d4_fk values (1);
+step s2detach: <... completed>
+error in steps s1insert s2detach: ERROR:  insert or update on table "d4_fk" violates foreign key constraint "d4_fk_a_fkey"
+step s1c: commit;
+
+starting permutation: s2snitch s1brr s1s s2detach s1cancel s1insert s1c
+step s2snitch: insert into d4_pid select pg_backend_pid();
+step s1brr: begin isolation level repeatable read;
+step s1s: select * from d4_primary;
+a              
+
+1              
+2              
+step s2detach: alter table d4_primary detach partition d4_primary1 concurrently; <waiting ...>
+step s1cancel: select pg_cancel_backend(pid) from d4_pid;
+pg_cancel_backend
+
+t              
+step s2detach: <... completed>
+error in steps s1cancel s2detach: ERROR:  canceling statement due to user request
+step s1insert: insert into d4_fk values (1);
+ERROR:  insert or update on table "d4_fk" violates foreign key constraint "d4_fk_a_fkey"
+step s1c: commit;
+
+starting permutation: s2snitch s1brr s1s s2detach s1insert s1c
+step s2snitch: insert into d4_pid select pg_backend_pid();
+step s1brr: begin isolation level repeatable read;
+step s1s: select * from d4_primary;
+a              
+
+1              
+2              
+step s2detach: alter table d4_primary detach partition d4_primary1 concurrently; <waiting ...>
+step s1insert: insert into d4_fk values (1);
+step s2detach: <... completed>
+error in steps s1insert s2detach: ERROR:  insert or update on table "d4_fk" violates foreign key constraint "d4_fk_a_fkey"
+step s1c: commit;
+
+starting permutation: s2snitch s1b s1declare s2detach s1cancel s1fetchall s1insert s1c
+step s2snitch: insert into d4_pid select pg_backend_pid();
+step s1b: begin;
+step s1declare: declare f cursor for select * from d4_primary;
+step s2detach: alter table d4_primary detach partition d4_primary1 concurrently; <waiting ...>
+step s1cancel: select pg_cancel_backend(pid) from d4_pid;
+pg_cancel_backend
+
+t              
+step s2detach: <... completed>
+error in steps s1cancel s2detach: ERROR:  canceling statement due to user request
+step s1fetchall: fetch all from f;
+a              
+
+1              
+2              
+step s1insert: insert into d4_fk values (1);
+ERROR:  insert or update on table "d4_fk" violates foreign key constraint "d4_fk_a_fkey"
+step s1c: commit;
+
+starting permutation: s2snitch s1b s1declare s2detach s1fetchall s1insert s1c
+step s2snitch: insert into d4_pid select pg_backend_pid();
+step s1b: begin;
+step s1declare: declare f cursor for select * from d4_primary;
+step s2detach: alter table d4_primary detach partition d4_primary1 concurrently; <waiting ...>
+step s1fetchall: fetch all from f;
+a              
+
+1              
+2              
+step s1insert: insert into d4_fk values (1);
+step s2detach: <... completed>
+error in steps s1insert s2detach: ERROR:  insert or update on table "d4_fk" violates foreign key constraint "d4_fk_a_fkey"
+step s1c: commit;
+
+starting permutation: s2snitch s1b s1declare s2detach s1cancel s1svpt s1insert s1rollback s1fetchall s1c
+step s2snitch: insert into d4_pid select pg_backend_pid();
+step s1b: begin;
+step s1declare: declare f cursor for select * from d4_primary;
+step s2detach: alter table d4_primary detach partition d4_primary1 concurrently; <waiting ...>
+step s1cancel: select pg_cancel_backend(pid) from d4_pid;
+pg_cancel_backend
+
+t              
+step s2detach: <... completed>
+error in steps s1cancel s2detach: ERROR:  canceling statement due to user request
+step s1svpt: savepoint f;
+step s1insert: insert into d4_fk values (1);
+ERROR:  insert or update on table "d4_fk" violates foreign key constraint "d4_fk_a_fkey"
+step s1rollback: rollback to f;
+step s1fetchall: fetch all from f;
+a              
+
+1              
+2              
+step s1c: commit;
+
+starting permutation: s2snitch s1b s1declare s2detach s1svpt s1insert s1rollback s1fetchall s1c
+step s2snitch: insert into d4_pid select pg_backend_pid();
+step s1b: begin;
+step s1declare: declare f cursor for select * from d4_primary;
+step s2detach: alter table d4_primary detach partition d4_primary1 concurrently; <waiting ...>
+step s1svpt: savepoint f;
+step s1insert: insert into d4_fk values (1);
+ERROR:  insert or update on table "d4_fk" violates foreign key constraint "d4_fk_a_fkey"
+step s1rollback: rollback to f;
+step s1fetchall: fetch all from f;
+a              
+
+1              
+2              
+step s1c: commit;
+step s2detach: <... completed>
+
+starting permutation: s2snitch s1b s2detach s1declare s1cancel s1fetchall s1insert s1c
+step s2snitch: insert into d4_pid select pg_backend_pid();
+step s1b: begin;
+step s2detach: alter table d4_primary detach partition d4_primary1 concurrently;
+step s1declare: declare f cursor for select * from d4_primary;
+step s1cancel: select pg_cancel_backend(pid) from d4_pid;
+pg_cancel_backend
+
+t              
+step s1fetchall: fetch all from f;
+a              
+
+2              
+step s1insert: insert into d4_fk values (1);
+ERROR:  insert or update on table "d4_fk" violates foreign key constraint "d4_fk_a_fkey"
+step s1c: commit;
+
+starting permutation: s2snitch s1b s2detach s1declare s1fetchall s1insert s1c
+step s2snitch: insert into d4_pid select pg_backend_pid();
+step s1b: begin;
+step s2detach: alter table d4_primary detach partition d4_primary1 concurrently;
+step s1declare: declare f cursor for select * from d4_primary;
+step s1fetchall: fetch all from f;
+a              
+
+2              
+step s1insert: insert into d4_fk values (1);
+ERROR:  insert or update on table "d4_fk" violates foreign key constraint "d4_fk_a_fkey"
+step s1c: commit;
+
+starting permutation: s2snitch s1b s2detach s1declare s1cancel s1svpt s1insert s1rollback s1fetchall s1c
+step s2snitch: insert into d4_pid select pg_backend_pid();
+step s1b: begin;
+step s2detach: alter table d4_primary detach partition d4_primary1 concurrently;
+step s1declare: declare f cursor for select * from d4_primary;
+step s1cancel: select pg_cancel_backend(pid) from d4_pid;
+pg_cancel_backend
+
+t              
+step s1svpt: savepoint f;
+step s1insert: insert into d4_fk values (1);
+ERROR:  insert or update on table "d4_fk" violates foreign key constraint "d4_fk_a_fkey"
+step s1rollback: rollback to f;
+step s1fetchall: fetch all from f;
+a              
+
+2              
+step s1c: commit;
+
+starting permutation: s2snitch s1b s2detach s1declare s1svpt s1insert s1rollback s1fetchall s1c
+step s2snitch: insert into d4_pid select pg_backend_pid();
+step s1b: begin;
+step s2detach: alter table d4_primary detach partition d4_primary1 concurrently;
+step s1declare: declare f cursor for select * from d4_primary;
+step s1svpt: savepoint f;
+step s1insert: insert into d4_fk values (1);
+ERROR:  insert or update on table "d4_fk" violates foreign key constraint "d4_fk_a_fkey"
+step s1rollback: rollback to f;
+step s1fetchall: fetch all from f;
+a              
+
+2              
+step s1c: commit;
+
+starting permutation: s2snitch s1brr s1declare2 s1fetchone s2detach s1cancel s1updcur s1c
+step s2snitch: insert into d4_pid select pg_backend_pid();
+step s1brr: begin isolation level repeatable read;
+step s1declare2: declare f cursor for select * from d4_fk where a = 2;
+step s1fetchone: fetch 1 from f;
+a              
+
+2              
+step s2detach: alter table d4_primary detach partition d4_primary1 concurrently; <waiting ...>
+step s1cancel: select pg_cancel_backend(pid) from d4_pid;
+pg_cancel_backend
+
+t              
+step s2detach: <... completed>
+error in steps s1cancel s2detach: ERROR:  canceling statement due to user request
+step s1updcur: update d4_fk set a = 1 where current of f;
+ERROR:  insert or update on table "d4_fk" violates foreign key constraint "d4_fk_a_fkey"
+step s1c: commit;
+
+starting permutation: s2snitch s1brr s1declare2 s1fetchone s2detach s1updcur s1c
+step s2snitch: insert into d4_pid select pg_backend_pid();
+step s1brr: begin isolation level repeatable read;
+step s1declare2: declare f cursor for select * from d4_fk where a = 2;
+step s1fetchone: fetch 1 from f;
+a              
+
+2              
+step s2detach: alter table d4_primary detach partition d4_primary1 concurrently; <waiting ...>
+step s1updcur: update d4_fk set a = 1 where current of f;
+step s2detach: <... completed>
+error in steps s1updcur s2detach: ERROR:  insert or update on table "d4_fk" violates foreign key constraint "d4_fk_a_fkey"
+step s1c: commit;
+
+starting permutation: s2snitch s1brr s1declare2 s1fetchone s1updcur s2detach s1c
+step s2snitch: insert into d4_pid select pg_backend_pid();
+step s1brr: begin isolation level repeatable read;
+step s1declare2: declare f cursor for select * from d4_fk where a = 2;
+step s1fetchone: fetch 1 from f;
+a              
+
+2              
+step s1updcur: update d4_fk set a = 1 where current of f;
+step s2detach: alter table d4_primary detach partition d4_primary1 concurrently; <waiting ...>
+step s1c: commit;
+step s2detach: <... completed>
+error in steps s1c s2detach: ERROR:  removing partition "d4_primary1" violates foreign key constraint "d4_fk_a_fkey1"
+
+starting permutation: s2snitch s1b s1s s2detach s3insert s1c
+step s2snitch: insert into d4_pid select pg_backend_pid();
+step s1b: begin;
+step s1s: select * from d4_primary;
+a              
+
+1              
+2              
+step s2detach: alter table d4_primary detach partition d4_primary1 concurrently; <waiting ...>
+step s3insert: insert into d4_fk values (1);
+ERROR:  insert or update on table "d4_fk" violates foreign key constraint "d4_fk_a_fkey"
+step s1c: commit;
+step s2detach: <... completed>
+
+starting permutation: s2snitch s1b s1s s2detach s3brr s3insert s3commit s1cancel s1c
+step s2snitch: insert into d4_pid select pg_backend_pid();
+step s1b: begin;
+step s1s: select * from d4_primary;
+a              
+
+1              
+2              
+step s2detach: alter table d4_primary detach partition d4_primary1 concurrently; <waiting ...>
+step s3brr: begin isolation level repeatable read;
+step s3insert: insert into d4_fk values (1);
+ERROR:  insert or update on table "d4_fk" violates foreign key constraint "d4_fk_a_fkey"
+step s3commit: commit;
+step s1cancel: select pg_cancel_backend(pid) from d4_pid;
+pg_cancel_backend
+
+t              
+step s2detach: <... completed>
+error in steps s1cancel s2detach: ERROR:  canceling statement due to user request
+step s1c: commit;
+
+starting permutation: s2snitch s1b s1s s2detach s3brr s3insert s3commit s1c
+step s2snitch: insert into d4_pid select pg_backend_pid();
+step s1b: begin;
+step s1s: select * from d4_primary;
+a              
+
+1              
+2              
+step s2detach: alter table d4_primary detach partition d4_primary1 concurrently; <waiting ...>
+step s3brr: begin isolation level repeatable read;
+step s3insert: insert into d4_fk values (1);
+ERROR:  insert or update on table "d4_fk" violates foreign key constraint "d4_fk_a_fkey"
+step s3commit: commit;
+step s1c: commit;
+step s2detach: <... completed>
+
+starting permutation: s2snitch s1brr s1s s2detach s1cancel s3vacfreeze s1s s1insert s1c
+step s2snitch: insert into d4_pid select pg_backend_pid();
+step s1brr: begin isolation level repeatable read;
+step s1s: select * from d4_primary;
+a              
+
+1              
+2              
+step s2detach: alter table d4_primary detach partition d4_primary1 concurrently; <waiting ...>
+step s1cancel: select pg_cancel_backend(pid) from d4_pid;
+pg_cancel_backend
+
+t              
+step s2detach: <... completed>
+error in steps s1cancel s2detach: ERROR:  canceling statement due to user request
+step s3vacfreeze: vacuum freeze pg_catalog.pg_inherits;
+step s1s: select * from d4_primary;
+a              
+
+1              
+2              
+step s1insert: insert into d4_fk values (1);
+step s1c: commit;
+
+starting permutation: s2snitch s1b s1s s2detach s1cancel s3vacfreeze s1s s1insert s1c
+step s2snitch: insert into d4_pid select pg_backend_pid();
+step s1b: begin;
+step s1s: select * from d4_primary;
+a              
+
+1              
+2              
+step s2detach: alter table d4_primary detach partition d4_primary1 concurrently; <waiting ...>
+step s1cancel: select pg_cancel_backend(pid) from d4_pid;
+pg_cancel_backend
+
+t              
+step s2detach: <... completed>
+error in steps s1cancel s2detach: ERROR:  canceling statement due to user request
+step s3vacfreeze: vacuum freeze pg_catalog.pg_inherits;
+step s1s: select * from d4_primary;
+a              
+
+2              
+step s1insert: insert into d4_fk values (1);
+ERROR:  insert or update on table "d4_fk" violates foreign key constraint "d4_fk_a_fkey"
+step s1c: commit;
index 5d6b79e66e5581154176893df0702bbda98224dd..f4c01006fc12f42ef80a53a0a5e92f4af87c1060 100644 (file)
@@ -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
diff --git a/src/test/isolation/specs/detach-partition-concurrently-1.spec b/src/test/isolation/specs/detach-partition-concurrently-1.spec
new file mode 100644 (file)
index 0000000..6ca0ada
--- /dev/null
@@ -0,0 +1,69 @@
+# 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"
diff --git a/src/test/isolation/specs/detach-partition-concurrently-2.spec b/src/test/isolation/specs/detach-partition-concurrently-2.spec
new file mode 100644 (file)
index 0000000..9281c80
--- /dev/null
@@ -0,0 +1,41 @@
+# 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"
diff --git a/src/test/isolation/specs/detach-partition-concurrently-3.spec b/src/test/isolation/specs/detach-partition-concurrently-3.spec
new file mode 100644 (file)
index 0000000..4b70643
--- /dev/null
@@ -0,0 +1,66 @@
+# 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"
diff --git a/src/test/isolation/specs/detach-partition-concurrently-4.spec b/src/test/isolation/specs/detach-partition-concurrently-4.spec
new file mode 100644 (file)
index 0000000..89f5f72
--- /dev/null
@@ -0,0 +1,74 @@
+# 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"
index f270aebf3a55d3d04569bce87ad681f5849f0268..70f24e846da1f82d1fc75a273351de1574038ff2 100644 (file)
@@ -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
diff --git a/src/test/modules/delay_execution/expected/partition-removal-1.out b/src/test/modules/delay_execution/expected/partition-removal-1.out
new file mode 100644 (file)
index 0000000..427f41c
--- /dev/null
@@ -0,0 +1,175 @@
+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;
diff --git a/src/test/modules/delay_execution/specs/partition-removal-1.spec b/src/test/modules/delay_execution/specs/partition-removal-1.spec
new file mode 100644 (file)
index 0000000..5ee2750
--- /dev/null
@@ -0,0 +1,58 @@
+# 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"
index bb3f873f22a83eaad47300518a8df3679b28b3c2..ec14b060a637c65c6006c11836e9c677888ecf08 100644 (file)
@@ -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;
index 4cc55d852513e698ecb51586efad2c0e44f72e83..7a9c9252dc0adb2a1a0312dc2f32d55ed589cd65 100644 (file)
@@ -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