v9-0001-Fix-ON-CONFLICT-to-work-with-partitioned-tables.patch
text/plain
Filename: v9-0001-Fix-ON-CONFLICT-to-work-with-partitioned-tables.patch
Type: text/plain
Part: 0
Patch
Format: format-patch
Series: patch v9-0001
Subject: Fix ON CONFLICT to work with partitioned tables
| File | + | − |
|---|---|---|
| doc/src/sgml/ddl.sgml | 0 | 15 |
| doc/src/sgml/ref/insert.sgml | 8 | 0 |
| src/backend/catalog/partition.c | 69 | 14 |
| src/backend/executor/execMain.c | 4 | 0 |
| src/backend/executor/execPartition.c | 239 | 13 |
| src/backend/executor/nodeModifyTable.c | 71 | 11 |
| src/backend/parser/analyze.c | 0 | 7 |
| src/include/catalog/partition.h | 1 | 0 |
| src/include/nodes/execnodes.h | 21 | 4 |
| src/test/regress/expected/insert_conflict.out | 75 | 11 |
| src/test/regress/expected/triggers.out | 33 | 0 |
| src/test/regress/sql/insert_conflict.sql | 62 | 10 |
| src/test/regress/sql/triggers.sql | 33 | 0 |
From d385c307fbe98935661d7b983229eb5b2e2e6436 Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
Date: Tue, 20 Mar 2018 10:09:38 +0900
Subject: [PATCH v9] Fix ON CONFLICT to work with partitioned tables
Author: Amit Langote, Alvaro Herrera, Etsuro Fujita
---
doc/src/sgml/ddl.sgml | 15 --
doc/src/sgml/ref/insert.sgml | 8 +
src/backend/catalog/partition.c | 83 +++++++--
src/backend/executor/execMain.c | 4 +
src/backend/executor/execPartition.c | 252 ++++++++++++++++++++++++--
src/backend/executor/nodeModifyTable.c | 82 +++++++--
src/backend/parser/analyze.c | 7 -
src/include/catalog/partition.h | 1 +
src/include/nodes/execnodes.h | 25 ++-
src/test/regress/expected/insert_conflict.out | 86 +++++++--
src/test/regress/expected/triggers.out | 33 ++++
src/test/regress/sql/insert_conflict.sql | 72 +++++++-
src/test/regress/sql/triggers.sql | 33 ++++
13 files changed, 616 insertions(+), 85 deletions(-)
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 3a54ba9d5a..8805b88d82 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -3324,21 +3324,6 @@ ALTER TABLE measurement ATTACH PARTITION measurement_y2008m02
<listitem>
<para>
- Using the <literal>ON CONFLICT</literal> clause with partitioned tables
- will cause an error if the conflict target is specified (see
- <xref linkend="sql-on-conflict" /> for more details on how the clause
- works). Therefore, it is not possible to specify
- <literal>DO UPDATE</literal> as the alternative action, because
- specifying the conflict target is mandatory in that case. On the other
- hand, specifying <literal>DO NOTHING</literal> as the alternative action
- works fine provided the conflict target is not specified. In that case,
- unique constraints (or exclusion constraints) of the individual leaf
- partitions are considered.
- </para>
- </listitem>
-
- <listitem>
- <para>
When an <command>UPDATE</command> causes a row to move from one
partition to another, there is a chance that another concurrent
<command>UPDATE</command> or <command>DELETE</command> misses this row.
diff --git a/doc/src/sgml/ref/insert.sgml b/doc/src/sgml/ref/insert.sgml
index 134092fa9c..62e142fd8e 100644
--- a/doc/src/sgml/ref/insert.sgml
+++ b/doc/src/sgml/ref/insert.sgml
@@ -518,6 +518,14 @@ INSERT INTO <replaceable class="parameter">table_name</replaceable> [ AS <replac
not duplicate each other in terms of attributes constrained by an
arbiter index or constraint.
</para>
+
+ <para>
+ Note that it is currently not supported for the
+ <literal>ON CONFLICT DO UPDATE</literal> clause of an
+ <command>INSERT</command> applied to a partitioned table to update the
+ partition key of a conflicting row such that it requires the row be moved
+ to a new partition.
+ </para>
<tip>
<para>
It is often preferable to use unique index inference rather than
diff --git a/src/backend/catalog/partition.c b/src/backend/catalog/partition.c
index 53855f5088..bfe559490e 100644
--- a/src/backend/catalog/partition.c
+++ b/src/backend/catalog/partition.c
@@ -192,6 +192,7 @@ static int get_partition_bound_num_indexes(PartitionBoundInfo b);
static int get_greatest_modulus(PartitionBoundInfo b);
static uint64 compute_hash_value(int partnatts, FmgrInfo *partsupfunc,
Datum *values, bool *isnull);
+static Oid get_partition_parent_recurse(Relation inhRel, Oid relid, bool getroot);
/*
* RelationBuildPartitionDesc
@@ -1377,6 +1378,7 @@ check_default_allows_bound(Relation parent, Relation default_rel,
/*
* get_partition_parent
+ * Obtain direct parent of given relation
*
* Returns inheritance parent of a partition by scanning pg_inherits
*
@@ -1387,14 +1389,59 @@ check_default_allows_bound(Relation parent, Relation default_rel,
Oid
get_partition_parent(Oid relid)
{
- Form_pg_inherits form;
- Relation catalogRelation;
+ Relation inhRel;
+ Oid parentOid;
+
+ inhRel = heap_open(InheritsRelationId, AccessShareLock);
+
+ parentOid = get_partition_parent_recurse(inhRel, relid, false);
+ if (parentOid == InvalidOid)
+ elog(ERROR, "could not find parent of relation %u", relid);
+
+ heap_close(inhRel, AccessShareLock);
+
+ return parentOid;
+}
+
+/*
+ * get_partition_parent
+ * Obtain topmost ancestor of given relation
+ *
+ * Returns the topmost parent inheritance parent of a partition by scanning
+ * pg_inherits
+ *
+ * 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_root_parent(Oid relid)
+{
+ Relation inhRel;
+ Oid parentOid;
+
+ inhRel = heap_open(InheritsRelationId, AccessShareLock);
+
+ parentOid = get_partition_parent_recurse(inhRel, relid, true);
+ if (parentOid == InvalidOid)
+ elog(ERROR, "could not find root parent of relation %u", relid);
+
+ heap_close(inhRel, AccessShareLock);
+
+ return parentOid;
+}
+
+/*
+ * get_partition_parent_recurse
+ * Recursive part of get_partition_parent
+ */
+static Oid
+get_partition_parent_recurse(Relation inhRel, Oid relid, bool getroot)
+{
SysScanDesc scan;
ScanKeyData key[2];
HeapTuple tuple;
- Oid result;
-
- catalogRelation = heap_open(InheritsRelationId, AccessShareLock);
+ Oid result = InvalidOid;
ScanKeyInit(&key[0],
Anum_pg_inherits_inhrelid,
@@ -1405,18 +1452,26 @@ get_partition_parent(Oid relid)
BTEqualStrategyNumber, F_INT4EQ,
Int32GetDatum(1));
- scan = systable_beginscan(catalogRelation, InheritsRelidSeqnoIndexId, true,
+ /* Obtain the direct parent, and release resources before recursing */
+ scan = systable_beginscan(inhRel, InheritsRelidSeqnoIndexId, true,
NULL, 2, key);
-
tuple = systable_getnext(scan);
- if (!HeapTupleIsValid(tuple))
- elog(ERROR, "could not find tuple for parent of relation %u", relid);
-
- form = (Form_pg_inherits) GETSTRUCT(tuple);
- result = form->inhparent;
-
+ if (HeapTupleIsValid(tuple))
+ result = ((Form_pg_inherits) GETSTRUCT(tuple))->inhparent;
systable_endscan(scan);
- heap_close(catalogRelation, AccessShareLock);
+
+ /*
+ * If we were asked to recurse, do so now. Except that if we didn't get a
+ * valid parent, then the 'relid' argument was already the topmost parent,
+ * so return that.
+ */
+ if (getroot)
+ {
+ if (OidIsValid(result))
+ return get_partition_parent_recurse(inhRel, result, getroot);
+ else
+ return relid;
+ }
return result;
}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 890067757c..352553da4b 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1349,11 +1349,15 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
resultRelInfo->ri_FdwRoutine = GetFdwRoutineForRelation(resultRelationDesc, true);
else
resultRelInfo->ri_FdwRoutine = NULL;
+
+ /* The following fields are set later if needed */
resultRelInfo->ri_FdwState = NULL;
resultRelInfo->ri_usesFdwDirectModify = false;
resultRelInfo->ri_ConstraintExprs = NULL;
resultRelInfo->ri_junkFilter = NULL;
resultRelInfo->ri_projectReturning = NULL;
+ resultRelInfo->ri_onConflictArbiterIndexes = NIL;
+ resultRelInfo->ri_onConflictSet = NULL;
/*
* Partition constraint, which also includes the partition constraint of
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index ce9a4e16cf..d92879442c 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -15,10 +15,12 @@
#include "postgres.h"
#include "catalog/pg_inherits_fn.h"
+#include "catalog/pg_type.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
+#include "nodes/makefuncs.h"
#include "utils/lsyscache.h"
#include "utils/rls.h"
#include "utils/ruleutils.h"
@@ -36,6 +38,7 @@ static char *ExecBuildSlotPartitionKeyDescription(Relation rel,
Datum *values,
bool *isnull,
int maxfieldlen);
+static List *adjust_partition_tlist(List *tlist, TupleConversionMap *map);
/*
* ExecSetupPartitionTupleRouting - sets up information needed during
@@ -64,6 +67,8 @@ ExecSetupPartitionTupleRouting(ModifyTableState *mtstate, Relation rel)
int num_update_rri = 0,
update_rri_index = 0;
PartitionTupleRouting *proute;
+ int nparts;
+ ModifyTable *node = mtstate ? (ModifyTable *) mtstate->ps.plan : NULL;
/*
* Get the information about the partition tree after locking all the
@@ -74,20 +79,16 @@ ExecSetupPartitionTupleRouting(ModifyTableState *mtstate, Relation rel)
proute->partition_dispatch_info =
RelationGetPartitionDispatchInfo(rel, &proute->num_dispatch,
&leaf_parts);
- proute->num_partitions = list_length(leaf_parts);
- proute->partitions = (ResultRelInfo **) palloc(proute->num_partitions *
- sizeof(ResultRelInfo *));
+ proute->num_partitions = nparts = list_length(leaf_parts);
+ proute->partitions =
+ (ResultRelInfo **) palloc(nparts * sizeof(ResultRelInfo *));
proute->parent_child_tupconv_maps =
- (TupleConversionMap **) palloc0(proute->num_partitions *
- sizeof(TupleConversionMap *));
- proute->partition_oids = (Oid *) palloc(proute->num_partitions *
- sizeof(Oid));
+ (TupleConversionMap **) palloc0(nparts * sizeof(TupleConversionMap *));
+ proute->partition_oids = (Oid *) palloc(nparts * sizeof(Oid));
/* Set up details specific to the type of tuple routing we are doing. */
- if (mtstate && mtstate->operation == CMD_UPDATE)
+ if (node && node->operation == CMD_UPDATE)
{
- ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
-
update_rri = mtstate->resultRelInfo;
num_update_rri = list_length(node->plans);
proute->subplan_partition_offsets =
@@ -475,9 +476,6 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
&mtstate->ps, RelationGetDescr(partrel));
}
- Assert(proute->partitions[partidx] == NULL);
- proute->partitions[partidx] = leaf_part_rri;
-
/*
* Save a tuple conversion map to convert a tuple routed to this partition
* from the parent's type to the partition's.
@@ -487,6 +485,143 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
RelationGetDescr(partrel),
gettext_noop("could not convert row type"));
+ /*
+ * Initialize information about this partition that's needed to handle
+ * the ON CONFLICT clause.
+ */
+ if (node && node->onConflictAction != ONCONFLICT_NONE)
+ {
+ TupleConversionMap *map = proute->parent_child_tupconv_maps[partidx];
+ int firstVarno = mtstate->resultRelInfo[0].ri_RangeTableIndex;
+ Relation firstResultRel = mtstate->resultRelInfo[0].ri_RelationDesc;
+ TupleDesc partrelDesc = RelationGetDescr(partrel);
+ ExprContext *econtext = mtstate->ps.ps_ExprContext;
+ ListCell *lc;
+ List *arbiterIndexes = NIL;
+
+ /* Generate a list of arbiter indexes for the partition. */
+ foreach(lc, resultRelInfo->ri_onConflictArbiterIndexes)
+ {
+ Oid parentArbiterIndexOid = lfirst_oid(lc);
+ int i;
+
+ /*
+ * Find parentArbiterIndexOid's child in this partition and add it
+ * to my_arbiterindexes.
+ */
+ for (i = 0; i < leaf_part_rri->ri_NumIndices; i++)
+ {
+ Relation index = leaf_part_rri->ri_IndexRelationDescs[i];
+ Oid indexOid = RelationGetRelid(index);
+
+ if (parentArbiterIndexOid ==
+ get_partition_root_parent(indexOid))
+ arbiterIndexes = lappend_oid(arbiterIndexes, indexOid);
+ }
+ }
+ leaf_part_rri->ri_onConflictArbiterIndexes = arbiterIndexes;
+
+ if (node->onConflictAction == ONCONFLICT_UPDATE)
+ {
+ Assert(node->onConflictSet != NIL);
+ Assert(resultRelInfo->ri_onConflictSet != NULL);
+
+ /*
+ * If partition's tuple descriptor matches exactly with the root
+ * parent, we can simply use the parent's ON CONFLICT SET state.
+ */
+ if (map == NULL)
+ leaf_part_rri->ri_onConflictSet =
+ resultRelInfo->ri_onConflictSet;
+ else
+ {
+ List *onconflset;
+ TupleDesc tupDesc;
+
+ /*
+ * We need to translate expressions (Vars and TargetEntry's)
+ * in onConflictSet and onConflictWhere to account for
+ * differences in attribute numbers between the partition and
+ * the root parent.
+ */
+ leaf_part_rri->ri_onConflictSet =
+ palloc0(sizeof(OnConflictSetState));
+
+ /*
+ * We need to call map_partition_varattnos twice -- first to
+ * convert Vars referencing the EXCLUDED pseudo-relation
+ * (varno == INNER_VAR) and then Vars referencing main target
+ * relation (varno == firstVarno).
+ *
+ * Note that node->onConflictSet itself remains unmodified
+ * here, because a copy is made before changing any nodes.
+ */
+ onconflset = map_partition_varattnos(node->onConflictSet,
+ INNER_VAR,
+ partrel,
+ firstResultRel, NULL);
+ onconflset = map_partition_varattnos(onconflset,
+ firstVarno,
+ partrel,
+ firstResultRel, NULL);
+
+ /*
+ * The original list wouldn't contain entries for the
+ * partition's dropped attributes, which we must be accounted
+ * for because targetlist must have all the attributes of the
+ * underlying table including the dropped ones. Fix that and
+ * reorder target list entries if their resnos change as a
+ * result of the adjustment.
+ */
+ onconflset = adjust_partition_tlist(onconflset, map);
+
+ /*
+ * Caller must set mtstate->mt_conflproj's tuple descriptor to
+ * this one before trying to use it for projection.
+ */
+ tupDesc = ExecTypeFromTL(onconflset, partrelDesc->tdhasoid);
+ leaf_part_rri->ri_onConflictSet->proj =
+ ExecBuildProjectionInfo(onconflset, econtext,
+ mtstate->mt_conflproj,
+ &mtstate->ps, partrelDesc);
+ leaf_part_rri->ri_onConflictSet->projTupDesc = tupDesc;
+
+ if (node->onConflictWhere)
+ {
+ /*
+ * Convert the Vars to contain partition's atttribute
+ * numbers
+ */
+ List *onconflwhere;
+
+ /*
+ * Just like for onConflictSet, we need to call
+ * map_partition_varattnos twice.
+ *
+ * Again node->onConflictWhere itself remains unchanged,
+ * because a copy is made before changing any nodes.
+ */
+ onconflwhere = map_partition_varattnos((List *)
+ node->onConflictWhere,
+ INNER_VAR,
+ partrel,
+ firstResultRel, NULL);
+ /* Convert Vars referencing main target relation. */
+ onconflwhere = map_partition_varattnos((List *)
+ onconflwhere,
+ firstVarno,
+ partrel,
+ firstResultRel, NULL);
+ leaf_part_rri->ri_onConflictSet->where =
+ ExecInitQual(onconflwhere, &mtstate->ps);
+ }
+ }
+ }
+ }
+
+ Assert(proute->partitions[partidx] == NULL);
+ proute->partitions[partidx] = leaf_part_rri;
+
MemoryContextSwitchTo(oldContext);
return leaf_part_rri;
@@ -946,3 +1081,94 @@ ExecBuildSlotPartitionKeyDescription(Relation rel,
return buf.data;
}
+
+/*
+ * adjust_partition_tlist
+ * Adjust the targetlist entries for a given partition to account for
+ * attribute differences between parent and the partition
+ *
+ * The expressions have already been fixed, but we have to make sure that the
+ * target resnos match the partition's attribute numbers. This results in
+ * generating a copy of the original target list in which the entries appear
+ * in sorted order of resno, including both the existing entries (that may
+ * have their resno changed in-place) and the newly added entries.
+ *
+ * Scribbles on the input tlist, so callers must make sure to make a copy
+ * before passing it to us.
+ */
+static List *
+adjust_partition_tlist(List *tlist, TupleConversionMap *map)
+{
+ List *new_tlist = NIL;
+ TupleDesc tupdesc = map->outdesc;
+ AttrNumber *attrMap = map->attrMap;
+ int numattrs = tupdesc->natts;
+ int attrno;
+
+ for (attrno = 1; attrno <= numattrs; attrno++)
+ {
+ Form_pg_attribute att_tup = TupleDescAttr(tupdesc, attrno - 1);
+ TargetEntry *tle;
+
+ if (attrMap[attrno - 1] != 0)
+ {
+ /*
+ * Use the existing entry from the parent, but make sure to
+ * update the resno to match the partition's attno.
+ */
+ Assert(!att_tup->attisdropped);
+
+ /* Get the corresponding tlist entry from the given tlist */
+ tle = (TargetEntry *) list_nth(tlist, attrMap[attrno - 1] - 1);
+
+ /* Get the resno right */
+ if (tle->resno != attrno)
+ tle->resno = attrno;
+ }
+ else
+ {
+ /*
+ * This corresponds to a dropped attribute in the partition, for
+ * which we enerate a dummy entry with resno matching the
+ * partition's attno.
+ */
+ Node *expr;
+
+ Assert(att_tup->attisdropped);
+
+ /* Insert NULL for dropped column */
+ expr = (Node *) makeConst(INT4OID,
+ -1,
+ InvalidOid,
+ sizeof(int32),
+ (Datum) 0,
+ true, /* isnull */
+ true /* byval */ );
+
+ tle = makeTargetEntry((Expr *) expr,
+ attrno,
+ pstrdup(NameStr(att_tup->attname)),
+ false);
+ }
+
+ new_tlist = lappend(new_tlist, tle);
+ }
+
+ /* Sanity check on the order of entries in the new tlist. */
+#ifdef USE_ASSERT_CHECKING
+ {
+ TargetEntry *prev = NULL;
+ ListCell *lc;
+
+ foreach(lc, new_tlist)
+ {
+ TargetEntry *cur = lfirst(lc);
+
+ Assert(prev == NULL || cur->resno > prev->resno);
+ prev = cur;
+ }
+ }
+#endif
+
+ return new_tlist;
+}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 4fa2d7265f..a2cd276b1d 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -422,7 +422,7 @@ ExecInsert(ModifyTableState *mtstate,
bool specConflict;
List *arbiterIndexes;
- arbiterIndexes = node->arbiterIndexes;
+ arbiterIndexes = resultRelInfo->ri_onConflictArbiterIndexes;
/*
* Do a non-conclusive check for conflicts first.
@@ -1056,6 +1056,18 @@ lreplace:;
TupleConversionMap *tupconv_map;
/*
+ * Disallow an INSERT ON CONFLICT DO UPDATE that causes the
+ * original row to migrate to a different partition. Maybe this
+ * can be implemented some day, but it seems a fringe feature with
+ * little redeeming value.
+ */
+ if (((ModifyTable *) mtstate->ps.plan)->onConflictAction == ONCONFLICT_UPDATE)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("invalid ON UPDATE specification"),
+ errdetail("The result tuple would appear in a different partition than the original tuple.")));
+
+ /*
* When an UPDATE is run on a leaf partition, we will not have
* partition tuple routing set up. In that case, fail with
* partition constraint violation error.
@@ -1313,7 +1325,7 @@ ExecOnConflictUpdate(ModifyTableState *mtstate,
{
ExprContext *econtext = mtstate->ps.ps_ExprContext;
Relation relation = resultRelInfo->ri_RelationDesc;
- ExprState *onConflictSetWhere = resultRelInfo->ri_onConflictSetWhere;
+ ExprState *onConflictSetWhere = resultRelInfo->ri_onConflictSet->where;
HeapTupleData tuple;
HeapUpdateFailureData hufd;
LockTupleMode lockmode;
@@ -1462,7 +1474,7 @@ ExecOnConflictUpdate(ModifyTableState *mtstate,
}
/* Project the new tuple version */
- ExecProject(resultRelInfo->ri_onConflictSetProj);
+ ExecProject(resultRelInfo->ri_onConflictSet->proj);
/*
* Note that it is possible that the target tuple has been modified in
@@ -1639,6 +1651,7 @@ ExecPrepareTupleRouting(ModifyTableState *mtstate,
ResultRelInfo *targetRelInfo,
TupleTableSlot *slot)
{
+ ModifyTable *node;
int partidx;
ResultRelInfo *partrel;
HeapTuple tuple;
@@ -1720,6 +1733,19 @@ ExecPrepareTupleRouting(ModifyTableState *mtstate,
proute->partition_tuple_slot,
&slot);
+ /* Initialize information needed to handle ON CONFLICT DO UPDATE. */
+ Assert(mtstate != NULL);
+ node = (ModifyTable *) mtstate->ps.plan;
+ if (node->onConflictAction == ONCONFLICT_UPDATE)
+ {
+ Assert(mtstate->mt_existing != NULL);
+ ExecSetSlotDescriptor(mtstate->mt_existing,
+ RelationGetDescr(partrel->ri_RelationDesc));
+ Assert(mtstate->mt_conflproj != NULL);
+ ExecSetSlotDescriptor(mtstate->mt_conflproj,
+ partrel->ri_onConflictSet->projTupDesc);
+ }
+
return slot;
}
@@ -2347,11 +2373,15 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
mtstate->ps.ps_ExprContext = NULL;
}
+ /* Set the list of arbiter indexes if needed for ON CONFLICT */
+ resultRelInfo = mtstate->resultRelInfo;
+ if (node->onConflictAction != ONCONFLICT_NONE)
+ resultRelInfo->ri_onConflictArbiterIndexes = node->arbiterIndexes;
+
/*
* If needed, Initialize target list, projection and qual for ON CONFLICT
* DO UPDATE.
*/
- resultRelInfo = mtstate->resultRelInfo;
if (node->onConflictAction == ONCONFLICT_UPDATE)
{
ExprContext *econtext;
@@ -2368,21 +2398,51 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
econtext = mtstate->ps.ps_ExprContext;
relationDesc = resultRelInfo->ri_RelationDesc->rd_att;
- /* initialize slot for the existing tuple */
- mtstate->mt_existing =
- ExecInitExtraTupleSlot(mtstate->ps.state, relationDesc);
+ /*
+ * Initialize slot for the existing tuple. If we'll be performing
+ * tuple routing, the tuple descriptor to use for this will be
+ * determined based on which relation the update is actually applied
+ * to, so we don't set its tuple descriptor here.
+ */
+ if (mtstate->mt_partition_tuple_routing == NULL)
+ mtstate->mt_existing =
+ ExecInitExtraTupleSlot(mtstate->ps.state, relationDesc);
+ else
+ mtstate->mt_existing =
+ ExecInitExtraTupleSlot(mtstate->ps.state, NULL);
/* carried forward solely for the benefit of explain */
mtstate->mt_excludedtlist = node->exclRelTlist;
+ /* create state for DO UPDATE SET operation */
+ resultRelInfo->ri_onConflictSet = palloc0(sizeof(OnConflictSetState));
+
/* create target slot for UPDATE SET projection */
tupDesc = ExecTypeFromTL((List *) node->onConflictSet,
relationDesc->tdhasoid);
- mtstate->mt_conflproj =
- ExecInitExtraTupleSlot(mtstate->ps.state, tupDesc);
+
+ /*
+ * Just like the "existing tuple" slot, we leave this slot's
+ * tuple descriptor set to NULL in the tuple routing case.
+ */
+ if (mtstate->mt_partition_tuple_routing == NULL)
+ mtstate->mt_conflproj =
+ ExecInitExtraTupleSlot(mtstate->ps.state, tupDesc);
+ else
+ mtstate->mt_conflproj =
+ ExecInitExtraTupleSlot(mtstate->ps.state, NULL);
+
+ /*
+ * Although, we keep this tuple descriptor around so that for the
+ * common case where partitions have the same descriptor as the root
+ * parent (this table), we don't end up regenerating it needlessly.
+ * ExecPrepareTupleRouting still has to set mtstate->mt_conflproj's
+ * descriptor though.
+ */
+ resultRelInfo->ri_onConflictSet->projTupDesc = tupDesc;
/* build UPDATE SET projection state */
- resultRelInfo->ri_onConflictSetProj =
+ resultRelInfo->ri_onConflictSet->proj =
ExecBuildProjectionInfo(node->onConflictSet, econtext,
mtstate->mt_conflproj, &mtstate->ps,
relationDesc);
@@ -2395,7 +2455,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
qualexpr = ExecInitQual((List *) node->onConflictWhere,
&mtstate->ps);
- resultRelInfo->ri_onConflictSetWhere = qualexpr;
+ resultRelInfo->ri_onConflictSet->where = qualexpr;
}
}
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index cf1a34e41a..a4b5aaef44 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -1026,13 +1026,6 @@ transformOnConflictClause(ParseState *pstate,
TargetEntry *te;
int attno;
- if (targetrel->rd_partdesc)
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("%s cannot be applied to partitioned table \"%s\"",
- "ON CONFLICT DO UPDATE",
- RelationGetRelationName(targetrel))));
-
/*
* All INSERT expressions have been parsed, get ready for potentially
* existing SET statements that need to be processed like an UPDATE.
diff --git a/src/include/catalog/partition.h b/src/include/catalog/partition.h
index 2faf0ca26e..287642b01b 100644
--- a/src/include/catalog/partition.h
+++ b/src/include/catalog/partition.h
@@ -52,6 +52,7 @@ extern PartitionBoundInfo partition_bounds_copy(PartitionBoundInfo src,
extern void check_new_partition_bound(char *relname, Relation parent,
PartitionBoundSpec *spec);
extern Oid get_partition_parent(Oid relid);
+extern Oid get_partition_root_parent(Oid relid);
extern List *get_qual_from_partbound(Relation rel, Relation parent,
PartitionBoundSpec *spec);
extern List *map_partition_varattnos(List *expr, int fromrel_varno,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index bf2616a95e..7d32927289 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -363,6 +363,24 @@ typedef struct JunkFilter
} JunkFilter;
/*
+ * OnConflictSetState
+ *
+ * Contains execution time state of a ON CONFLICT DO UPDATE operation, which
+ * includes the state of projection, tuple descriptor of the projection, and
+ * WHERE quals if any.
+ */
+typedef struct OnConflictSetState
+{ /* for computing ON CONFLICT DO UPDATE SET */
+ ProjectionInfo *proj;
+
+ /* TupleDesc describing the result of the above */
+ TupleDesc projTupDesc;
+
+ /* list of ON CONFLICT DO UPDATE exprs (qual) */
+ ExprState *where;
+} OnConflictSetState;
+
+/*
* ResultRelInfo
*
* Whenever we update an existing relation, we have to update indexes on the
@@ -424,11 +442,10 @@ typedef struct ResultRelInfo
/* for computing a RETURNING list */
ProjectionInfo *ri_projectReturning;
- /* for computing ON CONFLICT DO UPDATE SET */
- ProjectionInfo *ri_onConflictSetProj;
+ /* list of arbiter indexes to use to check conflicts */
+ List *ri_onConflictArbiterIndexes;
- /* list of ON CONFLICT DO UPDATE exprs (qual) */
- ExprState *ri_onConflictSetWhere;
+ OnConflictSetState *ri_onConflictSet;
/* partition check expression */
List *ri_PartitionCheck;
diff --git a/src/test/regress/expected/insert_conflict.out b/src/test/regress/expected/insert_conflict.out
index 2650faedee..6f1e3094d7 100644
--- a/src/test/regress/expected/insert_conflict.out
+++ b/src/test/regress/expected/insert_conflict.out
@@ -786,16 +786,80 @@ select * from selfconflict;
(3 rows)
drop table selfconflict;
--- check that the following works:
--- insert into partitioned_table on conflict do nothing
-create table parted_conflict_test (a int, b char) partition by list (a);
-create table parted_conflict_test_1 partition of parted_conflict_test (b unique) for values in (1);
+-- check ON CONFLICT handling with partitioned tables
+create table parted_conflict_test (a int unique, b char) partition by list (a);
+create table parted_conflict_test_1 partition of parted_conflict_test (b unique) for values in (1, 2);
+-- no indexes required here
insert into parted_conflict_test values (1, 'a') on conflict do nothing;
-insert into parted_conflict_test values (1, 'a') on conflict do nothing;
--- however, on conflict do update is not supported yet
-insert into parted_conflict_test values (1) on conflict (b) do update set a = excluded.a;
-ERROR: ON CONFLICT DO UPDATE cannot be applied to partitioned table "parted_conflict_test"
--- but it works OK if we target the partition directly
-insert into parted_conflict_test_1 values (1) on conflict (b) do
-update set a = excluded.a;
+-- index on a required, which does exist in parent
+insert into parted_conflict_test values (1, 'a') on conflict (a) do nothing;
+insert into parted_conflict_test values (1, 'a') on conflict (a) do update set b = excluded.b;
+-- targeting partition directly will work
+insert into parted_conflict_test_1 values (1, 'a') on conflict (a) do nothing;
+insert into parted_conflict_test_1 values (1, 'b') on conflict (a) do update set b = excluded.b;
+-- index on b required, which doesn't exist in parent
+insert into parted_conflict_test values (2, 'b') on conflict (b) do update set a = excluded.a;
+ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification
+-- targeting partition directly will work
+insert into parted_conflict_test_1 values (2, 'b') on conflict (b) do update set a = excluded.a;
+-- should see (2, 'b')
+select * from parted_conflict_test order by a;
+ a | b
+---+---
+ 2 | b
+(1 row)
+
+-- now check that DO UPDATE works correctly for target partition with
+-- different attribute numbers
+create table parted_conflict_test_2 (b char, a int unique);
+alter table parted_conflict_test attach partition parted_conflict_test_2 for values in (3);
+truncate parted_conflict_test;
+insert into parted_conflict_test values (3, 'a') on conflict (a) do update set b = excluded.b;
+insert into parted_conflict_test values (3, 'b') on conflict (a) do update set b = excluded.b;
+-- should see (3, 'b')
+select * from parted_conflict_test order by a;
+ a | b
+---+---
+ 3 | b
+(1 row)
+
+-- case where parent will have a dropped column, but the partition won't
+alter table parted_conflict_test drop b, add b char;
+create table parted_conflict_test_3 partition of parted_conflict_test for values in (4);
+truncate parted_conflict_test;
+insert into parted_conflict_test (a, b) values (4, 'a') on conflict (a) do update set b = excluded.b;
+insert into parted_conflict_test (a, b) values (4, 'b') on conflict (a) do update set b = excluded.b where parted_conflict_test.b = 'a';
+-- should see (4, 'b')
+select * from parted_conflict_test order by a;
+ a | b
+---+---
+ 4 | b
+(1 row)
+
+-- case with multi-level partitioning
+create table parted_conflict_test_4 partition of parted_conflict_test for values in (5) partition by list (a);
+create table parted_conflict_test_4_1 partition of parted_conflict_test_4 for values in (5);
+truncate parted_conflict_test;
+insert into parted_conflict_test (a, b) values (5, 'a') on conflict (a) do update set b = excluded.b;
+insert into parted_conflict_test (a, b) values (5, 'b') on conflict (a) do update set b = excluded.b where parted_conflict_test.b = 'a';
+-- should see (5, 'b')
+select * from parted_conflict_test order by a;
+ a | b
+---+---
+ 5 | b
+(1 row)
+
+-- test with multiple rows
+truncate parted_conflict_test;
+insert into parted_conflict_test (a, b) values (1, 'a'), (2, 'a'), (4, 'a') on conflict (a) do update set b = excluded.b where excluded.b = 'b';
+insert into parted_conflict_test (a, b) values (1, 'b'), (2, 'c'), (4, 'b') on conflict (a) do update set b = excluded.b where excluded.b = 'b';
+-- should see (1, 'b'), (2, 'a'), (4, 'b')
+select * from parted_conflict_test order by a;
+ a | b
+---+---
+ 1 | b
+ 2 | a
+ 4 | b
+(3 rows)
+
drop table parted_conflict_test;
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 99be9ac6e9..f53ac6bdf1 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2328,6 +2328,39 @@ insert into my_table values (3, 'CCC'), (4, 'DDD')
NOTICE: trigger = my_table_update_trig, old table = (3,CCC), (4,DDD), new table = (3,CCC:CCC), (4,DDD:DDD)
NOTICE: trigger = my_table_insert_trig, new table = <NULL>
--
+-- now using a partitioned table
+--
+create table iocdu_tt_parted (a int primary key, b text) partition by list (a);
+create table iocdu_tt_parted1 partition of iocdu_tt_parted for values in (1);
+create table iocdu_tt_parted2 partition of iocdu_tt_parted for values in (2);
+create table iocdu_tt_parted3 partition of iocdu_tt_parted for values in (3);
+create table iocdu_tt_parted4 partition of iocdu_tt_parted for values in (4);
+create trigger iocdu_tt_parted_insert_trig
+ after insert on iocdu_tt_parted referencing new table as new_table
+ for each statement execute procedure dump_insert();
+create trigger iocdu_tt_parted_update_trig
+ after update on iocdu_tt_parted referencing old table as old_table new table as new_table
+ for each statement execute procedure dump_update();
+-- inserts only
+insert into iocdu_tt_parted values (1, 'AAA'), (2, 'BBB')
+ on conflict (a) do
+ update set b = iocdu_tt_parted.b || ':' || excluded.b;
+NOTICE: trigger = iocdu_tt_parted_update_trig, old table = <NULL>, new table = <NULL>
+NOTICE: trigger = iocdu_tt_parted_insert_trig, new table = (1,AAA), (2,BBB)
+-- mixture of inserts and updates
+insert into iocdu_tt_parted values (1, 'AAA'), (2, 'BBB'), (3, 'CCC'), (4, 'DDD')
+ on conflict (a) do
+ update set b = iocdu_tt_parted.b || ':' || excluded.b;
+NOTICE: trigger = iocdu_tt_parted_update_trig, old table = (1,AAA), (2,BBB), new table = (1,AAA:AAA), (2,BBB:BBB)
+NOTICE: trigger = iocdu_tt_parted_insert_trig, new table = (3,CCC), (4,DDD)
+-- updates only
+insert into iocdu_tt_parted values (3, 'CCC'), (4, 'DDD')
+ on conflict (a) do
+ update set b = iocdu_tt_parted.b || ':' || excluded.b;
+NOTICE: trigger = iocdu_tt_parted_update_trig, old table = (3,CCC), (4,DDD), new table = (3,CCC:CCC), (4,DDD:DDD)
+NOTICE: trigger = iocdu_tt_parted_insert_trig, new table = <NULL>
+drop table iocdu_tt_parted;
+--
-- Verify that you can't create a trigger with transition tables for
-- more than one event.
--
diff --git a/src/test/regress/sql/insert_conflict.sql b/src/test/regress/sql/insert_conflict.sql
index 32c647e3f8..a25cd718a5 100644
--- a/src/test/regress/sql/insert_conflict.sql
+++ b/src/test/regress/sql/insert_conflict.sql
@@ -472,15 +472,67 @@ select * from selfconflict;
drop table selfconflict;
--- check that the following works:
--- insert into partitioned_table on conflict do nothing
-create table parted_conflict_test (a int, b char) partition by list (a);
-create table parted_conflict_test_1 partition of parted_conflict_test (b unique) for values in (1);
+-- check ON CONFLICT handling with partitioned tables
+create table parted_conflict_test (a int unique, b char) partition by list (a);
+create table parted_conflict_test_1 partition of parted_conflict_test (b unique) for values in (1, 2);
+
+-- no indexes required here
insert into parted_conflict_test values (1, 'a') on conflict do nothing;
-insert into parted_conflict_test values (1, 'a') on conflict do nothing;
--- however, on conflict do update is not supported yet
-insert into parted_conflict_test values (1) on conflict (b) do update set a = excluded.a;
--- but it works OK if we target the partition directly
-insert into parted_conflict_test_1 values (1) on conflict (b) do
-update set a = excluded.a;
+
+-- index on a required, which does exist in parent
+insert into parted_conflict_test values (1, 'a') on conflict (a) do nothing;
+insert into parted_conflict_test values (1, 'a') on conflict (a) do update set b = excluded.b;
+
+-- targeting partition directly will work
+insert into parted_conflict_test_1 values (1, 'a') on conflict (a) do nothing;
+insert into parted_conflict_test_1 values (1, 'b') on conflict (a) do update set b = excluded.b;
+
+-- index on b required, which doesn't exist in parent
+insert into parted_conflict_test values (2, 'b') on conflict (b) do update set a = excluded.a;
+
+-- targeting partition directly will work
+insert into parted_conflict_test_1 values (2, 'b') on conflict (b) do update set a = excluded.a;
+
+-- should see (2, 'b')
+select * from parted_conflict_test order by a;
+
+-- now check that DO UPDATE works correctly for target partition with
+-- different attribute numbers
+create table parted_conflict_test_2 (b char, a int unique);
+alter table parted_conflict_test attach partition parted_conflict_test_2 for values in (3);
+truncate parted_conflict_test;
+insert into parted_conflict_test values (3, 'a') on conflict (a) do update set b = excluded.b;
+insert into parted_conflict_test values (3, 'b') on conflict (a) do update set b = excluded.b;
+
+-- should see (3, 'b')
+select * from parted_conflict_test order by a;
+
+-- case where parent will have a dropped column, but the partition won't
+alter table parted_conflict_test drop b, add b char;
+create table parted_conflict_test_3 partition of parted_conflict_test for values in (4);
+truncate parted_conflict_test;
+insert into parted_conflict_test (a, b) values (4, 'a') on conflict (a) do update set b = excluded.b;
+insert into parted_conflict_test (a, b) values (4, 'b') on conflict (a) do update set b = excluded.b where parted_conflict_test.b = 'a';
+
+-- should see (4, 'b')
+select * from parted_conflict_test order by a;
+
+-- case with multi-level partitioning
+create table parted_conflict_test_4 partition of parted_conflict_test for values in (5) partition by list (a);
+create table parted_conflict_test_4_1 partition of parted_conflict_test_4 for values in (5);
+truncate parted_conflict_test;
+insert into parted_conflict_test (a, b) values (5, 'a') on conflict (a) do update set b = excluded.b;
+insert into parted_conflict_test (a, b) values (5, 'b') on conflict (a) do update set b = excluded.b where parted_conflict_test.b = 'a';
+
+-- should see (5, 'b')
+select * from parted_conflict_test order by a;
+
+-- test with multiple rows
+truncate parted_conflict_test;
+insert into parted_conflict_test (a, b) values (1, 'a'), (2, 'a'), (4, 'a') on conflict (a) do update set b = excluded.b where excluded.b = 'b';
+insert into parted_conflict_test (a, b) values (1, 'b'), (2, 'c'), (4, 'b') on conflict (a) do update set b = excluded.b where excluded.b = 'b';
+
+-- should see (1, 'b'), (2, 'a'), (4, 'b')
+select * from parted_conflict_test order by a;
+
drop table parted_conflict_test;
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 3354f4899f..3773c6bc98 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -1773,6 +1773,39 @@ insert into my_table values (3, 'CCC'), (4, 'DDD')
update set b = my_table.b || ':' || excluded.b;
--
+-- now using a partitioned table
+--
+
+create table iocdu_tt_parted (a int primary key, b text) partition by list (a);
+create table iocdu_tt_parted1 partition of iocdu_tt_parted for values in (1);
+create table iocdu_tt_parted2 partition of iocdu_tt_parted for values in (2);
+create table iocdu_tt_parted3 partition of iocdu_tt_parted for values in (3);
+create table iocdu_tt_parted4 partition of iocdu_tt_parted for values in (4);
+create trigger iocdu_tt_parted_insert_trig
+ after insert on iocdu_tt_parted referencing new table as new_table
+ for each statement execute procedure dump_insert();
+create trigger iocdu_tt_parted_update_trig
+ after update on iocdu_tt_parted referencing old table as old_table new table as new_table
+ for each statement execute procedure dump_update();
+
+-- inserts only
+insert into iocdu_tt_parted values (1, 'AAA'), (2, 'BBB')
+ on conflict (a) do
+ update set b = iocdu_tt_parted.b || ':' || excluded.b;
+
+-- mixture of inserts and updates
+insert into iocdu_tt_parted values (1, 'AAA'), (2, 'BBB'), (3, 'CCC'), (4, 'DDD')
+ on conflict (a) do
+ update set b = iocdu_tt_parted.b || ':' || excluded.b;
+
+-- updates only
+insert into iocdu_tt_parted values (3, 'CCC'), (4, 'DDD')
+ on conflict (a) do
+ update set b = iocdu_tt_parted.b || ':' || excluded.b;
+
+drop table iocdu_tt_parted;
+
+--
-- Verify that you can't create a trigger with transition tables for
-- more than one event.
--
--
2.11.0