]> granicus.if.org Git - postgresql/commitdiff
ALTER TABLE: skip FK validation when it's safe to do so
authorAlvaro Herrera <alvherre@alvh.no-ip.org>
Mon, 27 Feb 2012 21:28:00 +0000 (18:28 -0300)
committerAlvaro Herrera <alvherre@alvh.no-ip.org>
Mon, 27 Feb 2012 22:10:24 +0000 (19:10 -0300)
We already skip rewriting the table in these cases, but we still force a
whole table scan to validate the data.  This can be skipped, and thus
we can make the whole ALTER TABLE operation just do some catalog touches
instead of scanning the table, when these two conditions hold:

(a) Old and new pg_constraint.conpfeqop match exactly.  This is actually
stronger than needed; we could loosen things by way of operator
families, but it'd require a lot more effort.

(b) The functions, if any, implementing a cast from the foreign type to
the primary opcintype are the same.  For this purpose, we can consider a
binary coercion equivalent to an exact type match.  When the opcintype
is polymorphic, require that the old and new foreign types match
exactly.  (Since ri_triggers.c does use the executor, the stronger check
for polymorphic types is no mere future-proofing.  However, no core type
exercises its necessity.)

Author: Noah Misch

Committer's note: catalog version bumped due to change of the Constraint
node.  I can't actually find any way to have such a node in a stored
rule, but given that we have "out" support for them, better be safe.

src/backend/commands/tablecmds.c
src/backend/nodes/copyfuncs.c
src/backend/nodes/equalfuncs.c
src/backend/nodes/outfuncs.c
src/backend/utils/adt/ri_triggers.c
src/include/catalog/catversion.h
src/include/nodes/parsenodes.h

index 28889c1f44040f7fdb6465f4e0c6d3c77580d7b5..cd4490a1c24e5954213d075d768cb5021cdc11a9 100644 (file)
@@ -276,6 +276,8 @@ static Oid transformFkeyCheckAttrs(Relation pkrel,
                                                int numattrs, int16 *attnums,
                                                Oid *opclasses);
 static void checkFkeyPermissions(Relation rel, int16 *attnums, int natts);
+static CoercionPathType findFkeyCast(Oid targetTypeId, Oid sourceTypeId,
+                        Oid *funcid);
 static void validateCheckConstraint(Relation rel, HeapTuple constrtup);
 static void validateForeignKeyConstraint(char *conname,
                                                         Relation rel, Relation pkrel,
@@ -358,6 +360,7 @@ static void ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMOD
 static void ATPostAlterTypeParse(Oid oldId, char *cmd,
                                         List **wqueue, LOCKMODE lockmode, bool rewrite);
 static void TryReuseIndex(Oid oldId, IndexStmt *stmt);
+static void TryReuseForeignKey(Oid oldId, Constraint *con);
 static void change_owner_fix_column_acls(Oid relationOid,
                                                         Oid oldOwnerId, Oid newOwnerId);
 static void change_owner_recurse_to_sequences(Oid relationOid,
@@ -5620,6 +5623,8 @@ ATAddForeignKeyConstraint(AlteredTableInfo *tab, Relation rel,
                                numpks;
        Oid                     indexOid;
        Oid                     constrOid;
+       bool            old_check_ok;
+       ListCell   *old_pfeqop_item = list_head(fkconstraint->old_conpfeqop);
 
        /*
         * Grab an exclusive lock on the pk table, so that someone doesn't delete
@@ -5736,6 +5741,13 @@ ATAddForeignKeyConstraint(AlteredTableInfo *tab, Relation rel,
                                (errcode(ERRCODE_INVALID_FOREIGN_KEY),
                                 errmsg("number of referencing and referenced columns for foreign key disagree")));
 
+       /*
+        * On the strength of a previous constraint, we might avoid scanning
+        * tables to validate this one.  See below.
+        */
+       old_check_ok = (fkconstraint->old_conpfeqop != NIL);
+       Assert(!old_check_ok || numfks == list_length(fkconstraint->old_conpfeqop));
+
        for (i = 0; i < numpks; i++)
        {
                Oid                     pktype = pktypoid[i];
@@ -5750,6 +5762,7 @@ ATAddForeignKeyConstraint(AlteredTableInfo *tab, Relation rel,
                Oid                     ppeqop;
                Oid                     ffeqop;
                int16           eqstrategy;
+               Oid                     pfeqop_right;
 
                /* We need several fields out of the pg_opclass entry */
                cla_ht = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclasses[i]));
@@ -5792,10 +5805,17 @@ ATAddForeignKeyConstraint(AlteredTableInfo *tab, Relation rel,
                pfeqop = get_opfamily_member(opfamily, opcintype, fktyped,
                                                                         eqstrategy);
                if (OidIsValid(pfeqop))
+               {
+                       pfeqop_right = fktyped;
                        ffeqop = get_opfamily_member(opfamily, fktyped, fktyped,
                                                                                 eqstrategy);
+               }
                else
-                       ffeqop = InvalidOid;    /* keep compiler quiet */
+               {
+                       /* keep compiler quiet */
+                       pfeqop_right = InvalidOid;
+                       ffeqop = InvalidOid;
+               }
 
                if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop)))
                {
@@ -5817,7 +5837,10 @@ ATAddForeignKeyConstraint(AlteredTableInfo *tab, Relation rel,
                        target_typeids[1] = opcintype;
                        if (can_coerce_type(2, input_typeids, target_typeids,
                                                                COERCION_IMPLICIT))
+                       {
                                pfeqop = ffeqop = ppeqop;
+                               pfeqop_right = opcintype;
+                       }
                }
 
                if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop)))
@@ -5833,6 +5856,77 @@ ATAddForeignKeyConstraint(AlteredTableInfo *tab, Relation rel,
                                                           format_type_be(fktype),
                                                           format_type_be(pktype))));
 
+               if (old_check_ok)
+               {
+                       /*
+                        * When a pfeqop changes, revalidate the constraint.  We could
+                        * permit intra-opfamily changes, but that adds subtle complexity
+                        * without any concrete benefit for core types.  We need not
+                        * assess ppeqop or ffeqop, which RI_Initial_Check() does not use.
+                        */
+                       old_check_ok = (pfeqop == lfirst_oid(old_pfeqop_item));
+                       old_pfeqop_item = lnext(old_pfeqop_item);
+               }
+               if (old_check_ok)
+               {
+                       Oid                     old_fktype;
+                       Oid                     new_fktype;
+                       CoercionPathType old_pathtype;
+                       CoercionPathType new_pathtype;
+                       Oid                     old_castfunc;
+                       Oid                     new_castfunc;
+
+                       /*
+                        * Identify coercion pathways from each of the old and new FK-side
+                        * column types to the right (foreign) operand type of the pfeqop.
+                        * We may assume that pg_constraint.conkey is not changing.
+                        */
+                       old_fktype = tab->oldDesc->attrs[fkattnum[i] - 1]->atttypid;
+                       new_fktype = fktype;
+                       old_pathtype = findFkeyCast(pfeqop_right, old_fktype,
+                                                                               &old_castfunc);
+                       new_pathtype = findFkeyCast(pfeqop_right, new_fktype,
+                                                                               &new_castfunc);
+
+                       /*
+                        * Upon a change to the cast from the FK column to its pfeqop
+                        * operand, revalidate the constraint.  For this evaluation, a
+                        * binary coercion cast is equivalent to no cast at all.  While
+                        * type implementors should design implicit casts with an eye
+                        * toward consistency of operations like equality, we cannot assume
+                        * here that they have done so.
+                        *
+                        * A function with a polymorphic argument could change behavior
+                        * arbitrarily in response to get_fn_expr_argtype().  Therefore,
+                        * when the cast destination is polymorphic, we only avoid
+                        * revalidation if the input type has not changed at all.  Given
+                        * just the core data types and operator classes, this requirement
+                        * prevents no would-be optimizations.
+                        *
+                        * If the cast converts from a base type to a domain thereon, then
+                        * that domain type must be the opcintype of the unique index.
+                        * Necessarily, the primary key column must then be of the domain
+                        * type.  Since the constraint was previously valid, all values on
+                        * the foreign side necessarily exist on the primary side and in
+                        * turn conform to the domain.  Consequently, we need not treat
+                        * domains specially here.
+                        *
+                        * Since we require that all collations share the same notion of
+                        * equality (which they do, because texteq reduces to bitwise
+                        * equality), we don't compare collation here.
+                        *
+                        * We need not directly consider the PK type.  It's necessarily
+                        * binary coercible to the opcintype of the unique index column,
+                        * and ri_triggers.c will only deal with PK datums in terms of that
+                        * opcintype.  Changing the opcintype also changes pfeqop.
+                        */
+                       old_check_ok = (new_pathtype == old_pathtype &&
+                                                       new_castfunc == old_castfunc &&
+                                                       (!IsPolymorphicType(pfeqop_right) ||
+                                                        new_fktype == old_fktype));
+
+               }
+
                pfeqoperators[i] = pfeqop;
                ppeqoperators[i] = ppeqop;
                ffeqoperators[i] = ffeqop;
@@ -5877,10 +5971,12 @@ ATAddForeignKeyConstraint(AlteredTableInfo *tab, Relation rel,
 
        /*
         * Tell Phase 3 to check that the constraint is satisfied by existing rows.
-        * We can skip this during table creation, or if requested explicitly by
-        * specifying NOT VALID in an ADD FOREIGN KEY command.
+        * We can skip this during table creation, when requested explicitly by
+        * specifying NOT VALID in an ADD FOREIGN KEY command, and when we're
+        * recreating a constraint following a SET DATA TYPE operation that did not
+        * impugn its validity.
         */
-       if (!fkconstraint->skip_validation)
+       if (!old_check_ok && !fkconstraint->skip_validation)
        {
                NewConstraint *newcon;
 
@@ -6330,6 +6426,35 @@ transformFkeyCheckAttrs(Relation pkrel,
        return indexoid;
 }
 
+/*
+ * findFkeyCast -
+ *
+ *     Wrapper around find_coercion_pathway() for ATAddForeignKeyConstraint().
+ *     Caller has equal regard for binary coercibility and for an exact match.
+*/
+static CoercionPathType
+findFkeyCast(Oid targetTypeId, Oid sourceTypeId, Oid *funcid)
+{
+       CoercionPathType ret;
+
+       if (targetTypeId == sourceTypeId)
+       {
+               ret = COERCION_PATH_RELABELTYPE;
+               *funcid = InvalidOid;
+       }
+       else
+       {
+               ret = find_coercion_pathway(targetTypeId, sourceTypeId,
+                                                                       COERCION_IMPLICIT, funcid);
+               if (ret == COERCION_PATH_NONE)
+                       /* A previously-relied-upon cast is now gone. */
+                       elog(ERROR, "could not find cast from %u to %u",
+                                sourceTypeId, targetTypeId);
+       }
+
+       return ret;
+}
+
 /* Permissions checks for ADD FOREIGN KEY */
 static void
 checkFkeyPermissions(Relation rel, int16 *attnums, int natts)
@@ -7717,6 +7842,7 @@ ATPostAlterTypeParse(Oid oldId, char *cmd,
                                        foreach(lcmd, stmt->cmds)
                                        {
                                                AlterTableCmd *cmd = (AlterTableCmd *) lfirst(lcmd);
+                                               Constraint *con;
 
                                                switch (cmd->subtype)
                                                {
@@ -7730,6 +7856,12 @@ ATPostAlterTypeParse(Oid oldId, char *cmd,
                                                                        lappend(tab->subcmds[AT_PASS_OLD_INDEX], cmd);
                                                                break;
                                                        case AT_AddConstraint:
+                                                               Assert(IsA(cmd->def, Constraint));
+                                                               con = (Constraint *) cmd->def;
+                                                               /* rewriting neither side of a FK */
+                                                               if (con->contype == CONSTR_FOREIGN &&
+                                                                       !rewrite && !tab->rewrite)
+                                                                       TryReuseForeignKey(oldId, con);
                                                                tab->subcmds[AT_PASS_OLD_CONSTR] =
                                                                        lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd);
                                                                break;
@@ -7751,7 +7883,7 @@ ATPostAlterTypeParse(Oid oldId, char *cmd,
 /*
  * Subroutine for ATPostAlterTypeParse().  Calls out to CheckIndexCompatible()
  * for the real analysis, then mutates the IndexStmt based on that verdict.
-*/
+ */
 static void
 TryReuseIndex(Oid oldId, IndexStmt *stmt)
 {
@@ -7768,6 +7900,50 @@ TryReuseIndex(Oid oldId, IndexStmt *stmt)
        }
 }
 
+/*
+ * Subroutine for ATPostAlterTypeParse().
+ *
+ * Stash the old P-F equality operator into the Constraint node, for possible
+ * use by ATAddForeignKeyConstraint() in determining whether revalidation of
+ * this constraint can be skipped.
+ */
+static void
+TryReuseForeignKey(Oid oldId, Constraint *con)
+{
+       HeapTuple       tup;
+       Datum           adatum;
+       bool            isNull;
+       ArrayType  *arr;
+       Oid                *rawarr;
+       int                     numkeys;
+       int                     i;
+
+       Assert(con->contype == CONSTR_FOREIGN);
+       Assert(con->old_conpfeqop == NIL); /* already prepared this node */
+
+       tup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(oldId));
+       if (!HeapTupleIsValid(tup)) /* should not happen */
+               elog(ERROR, "cache lookup failed for constraint %u", oldId);
+
+       adatum = SysCacheGetAttr(CONSTROID, tup,
+                                                        Anum_pg_constraint_conpfeqop, &isNull);
+       if (isNull)
+               elog(ERROR, "null conpfeqop for constraint %u", oldId);
+       arr = DatumGetArrayTypeP(adatum);       /* ensure not toasted */
+       numkeys = ARR_DIMS(arr)[0];
+       /* test follows the one in ri_FetchConstraintInfo() */
+       if (ARR_NDIM(arr) != 1 ||
+               ARR_HASNULL(arr) ||
+               ARR_ELEMTYPE(arr) != OIDOID)
+               elog(ERROR, "conpfeqop is not a 1-D Oid array");
+       rawarr = (Oid *) ARR_DATA_PTR(arr);
+
+       /* stash a List of the operator Oids in our Constraint node */
+       for (i = 0; i < numkeys; i++)
+               con->old_conpfeqop = lcons_oid(rawarr[i], con->old_conpfeqop);
+
+       ReleaseSysCache(tup);
+}
 
 /*
  * ALTER TABLE OWNER
index cc3168d906670ba1ca3ea4765fd9de5a8db343f6..7fec4dbf7b56c9c465059c236d9668ef775b97ee 100644 (file)
@@ -2364,6 +2364,7 @@ _copyConstraint(const Constraint *from)
        COPY_SCALAR_FIELD(fk_matchtype);
        COPY_SCALAR_FIELD(fk_upd_action);
        COPY_SCALAR_FIELD(fk_del_action);
+       COPY_NODE_FIELD(old_conpfeqop);
        COPY_SCALAR_FIELD(skip_validation);
        COPY_SCALAR_FIELD(initially_valid);
 
index 2295195fabc36a51d4687b1412424c702ca14a67..d2a79eb851c8cf7134a6f0decc7045de4f667c93 100644 (file)
@@ -2199,6 +2199,7 @@ _equalConstraint(const Constraint *a, const Constraint *b)
        COMPARE_SCALAR_FIELD(fk_matchtype);
        COMPARE_SCALAR_FIELD(fk_upd_action);
        COMPARE_SCALAR_FIELD(fk_del_action);
+       COMPARE_NODE_FIELD(old_conpfeqop);
        COMPARE_SCALAR_FIELD(skip_validation);
        COMPARE_SCALAR_FIELD(initially_valid);
 
index 829f6d4f7b59cebb0433d20c5a6c325aa5a2b178..25a215e9d71f761aae6c12d8fa34713048a43484 100644 (file)
@@ -2626,6 +2626,7 @@ _outConstraint(StringInfo str, const Constraint *node)
                        WRITE_CHAR_FIELD(fk_matchtype);
                        WRITE_CHAR_FIELD(fk_upd_action);
                        WRITE_CHAR_FIELD(fk_del_action);
+                       WRITE_NODE_FIELD(old_conpfeqop);
                        WRITE_BOOL_FIELD(skip_validation);
                        WRITE_BOOL_FIELD(initially_valid);
                        break;
index 03a974a7121b1ca4b2420620c3b86c9315ed9cb9..dd58f4efc8a372bcffaf82068f9e666ab7abe5f9 100644 (file)
@@ -3224,6 +3224,7 @@ ri_FetchConstraintInfo(RI_ConstraintInfo *riinfo,
                elog(ERROR, "null conpfeqop for constraint %u", constraintOid);
        arr = DatumGetArrayTypeP(adatum);       /* ensure not toasted */
        numkeys = ARR_DIMS(arr)[0];
+       /* see TryReuseForeignKey if you change the test below */
        if (ARR_NDIM(arr) != 1 ||
                numkeys != riinfo->nkeys ||
                numkeys > RI_MAX_NUMKEYS ||
index 6100472d94a918c2e461013605d54164282dd506..8451dfde040a34d2c107d3735e123105783feca7 100644 (file)
@@ -53,6 +53,6 @@
  */
 
 /*                                                     yyyymmddN */
-#define CATALOG_VERSION_NO     201202191
+#define CATALOG_VERSION_NO     201202271
 
 #endif
index 1d33cebc9b8ce371c87a0045ffc1b6df158b3490..ab5563997d409d40ceec4d0957438bbc89a17996 100644 (file)
@@ -1552,6 +1552,7 @@ typedef struct Constraint
        char            fk_matchtype;   /* FULL, PARTIAL, UNSPECIFIED */
        char            fk_upd_action;  /* ON UPDATE action */
        char            fk_del_action;  /* ON DELETE action */
+       List       *old_conpfeqop;      /* pg_constraint.conpfeqop of my former self */
 
        /* Fields used for constraints that allow a NOT VALID specification */
        bool            skip_validation;        /* skip validation of existing rows? */