Check equality semantics for unique indexes on partitioned tables.
authorTom Lane <[email protected]>
Wed, 1 Apr 2020 18:49:49 +0000 (14:49 -0400)
committerTom Lane <[email protected]>
Wed, 1 Apr 2020 18:49:49 +0000 (14:49 -0400)
We require the partition key to be a subset of the set of columns
being made unique, so that physically-separate indexes on the different
partitions are sufficient to enforce the uniqueness constraint.

The existing code checked that the listed columns appear, but did not
inquire into the index semantics, which is a serious oversight given
that different index opclasses might enforce completely different
notions of uniqueness.

Ideally, perhaps, we'd just match the partition key opfamily to the
index opfamily.  But hash partitioning uses hash opfamilies which we
can't directly match to btree opfamilies.  Hence, look up the equality
operator in each family, and accept if it's the same operator.  This
should be okay in a fairly general sense, since the equality operator
ought to precisely represent the opfamily's notion of uniqueness.

A remaining weak spot is that we don't have a cross-index-AM notion of
which opfamily member is "equality".  But we know which one to use for
hash and btree AMs, and those are the only two that are relevant here
at present.  (Any non-core AMs that know how to enforce equality are
out of luck, for now.)

Back-patch to v11 where this feature was introduced.

Guancheng Luo, revised a bit by me

Discussion: https://postgr.es/m/D9C3CEF7-04E8-47A1-8300-CA1DCD5ED40D@gmail.com

src/backend/commands/indexcmds.c

index 2e5997b5c3c37bad4305a120b80dcd812db71af8..b144e2301dc4f7f34497c1e65ff7eefae0cbe108 100644 (file)
@@ -871,9 +871,9 @@ DefineIndex(Oid relationId,
 
    /*
     * If this table is partitioned and we're creating a unique index or a
-    * primary key, make sure that the indexed columns are part of the
-    * partition key.  Otherwise it would be possible to violate uniqueness by
-    * putting values that ought to be unique in different partitions.
+    * primary key, make sure that the partition key is a subset of the
+    * index's columns.  Otherwise it would be possible to violate uniqueness
+    * by putting values that ought to be unique in different partitions.
     *
     * We could lift this limitation if we had global indexes, but those have
     * their own problems, so this is a useful feature combination.
@@ -881,34 +881,65 @@ DefineIndex(Oid relationId,
    if (partitioned && (stmt->unique || stmt->primary))
    {
        PartitionKey key = RelationGetPartitionKey(rel);
+       const char *constraint_type;
        int         i;
 
+       if (stmt->primary)
+           constraint_type = "PRIMARY KEY";
+       else if (stmt->unique)
+           constraint_type = "UNIQUE";
+       else if (stmt->excludeOpNames != NIL)
+           constraint_type = "EXCLUDE";
+       else
+       {
+           elog(ERROR, "unknown constraint type");
+           constraint_type = NULL; /* keep compiler quiet */
+       }
+
        /*
-        * A partitioned table can have unique indexes, as long as all the
-        * columns in the partition key appear in the unique key.  A
-        * partition-local index can enforce global uniqueness iff the PK
-        * value completely determines the partition that a row is in.
-        *
-        * Thus, verify that all the columns in the partition key appear in
-        * the unique key definition.
+        * Verify that all the columns in the partition key appear in the
+        * unique key definition, with the same notion of equality.
         */
        for (i = 0; i < key->partnatts; i++)
        {
            bool        found = false;
+           int         eq_strategy;
+           Oid         ptkey_eqop;
            int         j;
-           const char *constraint_type;
-
-           if (stmt->primary)
-               constraint_type = "PRIMARY KEY";
-           else if (stmt->unique)
-               constraint_type = "UNIQUE";
-           else if (stmt->excludeOpNames != NIL)
-               constraint_type = "EXCLUDE";
+
+           /*
+            * Identify the equality operator associated with this partkey
+            * column.  For list and range partitioning, partkeys use btree
+            * operator classes; hash partitioning uses hash operator classes.
+            * (Keep this in sync with ComputePartitionAttrs!)
+            */
+           if (key->strategy == PARTITION_STRATEGY_HASH)
+               eq_strategy = HTEqualStrategyNumber;
            else
-           {
-               elog(ERROR, "unknown constraint type");
-               constraint_type = NULL; /* keep compiler quiet */
-           }
+               eq_strategy = BTEqualStrategyNumber;
+
+           ptkey_eqop = get_opfamily_member(key->partopfamily[i],
+                                            key->partopcintype[i],
+                                            key->partopcintype[i],
+                                            eq_strategy);
+           if (!OidIsValid(ptkey_eqop))
+               elog(ERROR, "missing operator %d(%u,%u) in partition opfamily %u",
+                    eq_strategy, key->partopcintype[i], key->partopcintype[i],
+                    key->partopfamily[i]);
+
+           /*
+            * We'll need to be able to identify the equality operators
+            * associated with index columns, too.  We know what to do with
+            * btree opclasses; if there are ever any other index types that
+            * support unique indexes, this logic will need extension.
+            */
+           if (accessMethodId == BTREE_AM_OID)
+               eq_strategy = BTEqualStrategyNumber;
+           else
+               ereport(ERROR,
+                       (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                        errmsg("cannot match partition key to an index using access method \"%s\"",
+                               accessMethodName)));
 
            /*
             * It may be possible to support UNIQUE constraints when partition
@@ -922,19 +953,40 @@ DefineIndex(Oid relationId,
                         errdetail("%s constraints cannot be used when partition keys include expressions.",
                                   constraint_type)));
 
+           /* Search the index column(s) for a match */
            for (j = 0; j < indexInfo->ii_NumIndexKeyAttrs; j++)
            {
                if (key->partattrs[i] == indexInfo->ii_IndexAttrNumbers[j])
                {
-                   found = true;
-                   break;
+                   /* Matched the column, now what about the equality op? */
+                   Oid         idx_opfamily;
+                   Oid         idx_opcintype;
+
+                   if (get_opclass_opfamily_and_input_type(classObjectId[j],
+                                                           &idx_opfamily,
+                                                           &idx_opcintype))
+                   {
+                       Oid         idx_eqop;
+
+                       idx_eqop = get_opfamily_member(idx_opfamily,
+                                                      idx_opcintype,
+                                                      idx_opcintype,
+                                                      eq_strategy);
+                       if (ptkey_eqop == idx_eqop)
+                       {
+                           found = true;
+                           break;
+                       }
+                   }
                }
            }
+
            if (!found)
            {
                Form_pg_attribute att;
 
-               att = TupleDescAttr(RelationGetDescr(rel), key->partattrs[i] - 1);
+               att = TupleDescAttr(RelationGetDescr(rel),
+                                   key->partattrs[i] - 1);
                ereport(ERROR,
                        (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                         errmsg("insufficient columns in %s constraint definition",