Fix optimization of foreign-key on update actions
authorPeter Eisentraut <[email protected]>
Mon, 18 Mar 2019 16:01:40 +0000 (17:01 +0100)
committerPeter Eisentraut <[email protected]>
Mon, 18 Mar 2019 16:19:21 +0000 (17:19 +0100)
In RI_FKey_pk_upd_check_required(), we check among other things
whether the old and new key are equal, so that we don't need to run
cascade actions when nothing has actually changed.  This was using the
equality operator.  But the effect of this is that if a value in the
primary key is changed to one that "looks" different but compares as
equal, the update is not propagated.  (Examples are float -0 and 0 and
case-insensitive text.)  This appears to violate the SQL standard, and
it also behaves inconsistently if in a multicolumn key another key is
also updated that would cause the row to compare as not equal.

To fix, if we are looking at the PK table in ri_KeysEqual(), then do a
bytewise comparison similar to record_image_eq() instead of using the
equality operators.  This only makes a difference for ON UPDATE
CASCADE, but for consistency we treat all changes to the PK the same.  For
the FK table, we continue to use the equality operators.

Discussion: https://www.postgresql.org/message-id/flat/3326fc2e-bc02-d4c5-e3e5-e54da466e89a@2ndquadrant.com

src/backend/utils/adt/datum.c
src/backend/utils/adt/ri_triggers.c
src/backend/utils/adt/rowtypes.c
src/include/utils/datum.h
src/test/regress/expected/foreign_key.out
src/test/regress/sql/foreign_key.sql

index af5944d3954da31b0698f28d74548f6f7c6e682b..81ea5a48e597bc64520488c5054b956585681fb1 100644 (file)
@@ -42,6 +42,8 @@
 
 #include "postgres.h"
 
+#include "access/tuptoaster.h"
+#include "fmgr.h"
 #include "utils/datum.h"
 #include "utils/expandeddatum.h"
 
@@ -251,6 +253,61 @@ datumIsEqual(Datum value1, Datum value2, bool typByVal, int typLen)
    return res;
 }
 
+/*-------------------------------------------------------------------------
+ * datum_image_eq
+ *
+ * Compares two datums for identical contents, based on byte images.  Return
+ * true if the two datums are equal, false otherwise.
+ *-------------------------------------------------------------------------
+ */
+bool
+datum_image_eq(Datum value1, Datum value2, bool typByVal, int typLen)
+{
+   bool        result = true;
+
+   if (typLen == -1)
+   {
+       Size        len1,
+                   len2;
+
+       len1 = toast_raw_datum_size(value1);
+       len2 = toast_raw_datum_size(value2);
+       /* No need to de-toast if lengths don't match. */
+       if (len1 != len2)
+           result = false;
+       else
+       {
+           struct varlena *arg1val;
+           struct varlena *arg2val;
+
+           arg1val = PG_DETOAST_DATUM_PACKED(value1);
+           arg2val = PG_DETOAST_DATUM_PACKED(value2);
+
+           result = (memcmp(VARDATA_ANY(arg1val),
+                            VARDATA_ANY(arg2val),
+                            len1 - VARHDRSZ) == 0);
+
+           /* Only free memory if it's a copy made here. */
+           if ((Pointer) arg1val != (Pointer) value1)
+               pfree(arg1val);
+           if ((Pointer) arg2val != (Pointer) value2)
+               pfree(arg2val);
+       }
+   }
+   else if (typByVal)
+   {
+       result = (value1 == value2);
+   }
+   else
+   {
+       result = (memcmp(DatumGetPointer(value1),
+                        DatumGetPointer(value2),
+                        typLen) == 0);
+   }
+
+   return result;
+}
+
 /*-------------------------------------------------------------------------
  * datumEstimateSpace
  *
index d715709b7cd145b5eae691778c1af840e48a8125..b4d0029877b5d3b8693b758353f2400cf6a8d121 100644 (file)
@@ -42,6 +42,7 @@
 #include "storage/bufmgr.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/fmgroids.h"
 #include "utils/guc.h"
 #include "utils/inval.h"
@@ -2402,18 +2403,11 @@ ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
             const RI_ConstraintInfo *riinfo, bool rel_is_pk)
 {
    const int16 *attnums;
-   const Oid  *eq_oprs;
 
    if (rel_is_pk)
-   {
        attnums = riinfo->pk_attnums;
-       eq_oprs = riinfo->pp_eq_oprs;
-   }
    else
-   {
        attnums = riinfo->fk_attnums;
-       eq_oprs = riinfo->ff_eq_oprs;
-   }
 
    /* XXX: could be worthwhile to fetch all necessary attrs at once */
    for (int i = 0; i < riinfo->nkeys; i++)
@@ -2436,12 +2430,32 @@ ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
        if (isnull)
            return false;
 
-       /*
-        * Compare them with the appropriate equality operator.
-        */
-       if (!ri_AttributesEqual(eq_oprs[i], RIAttType(rel, attnums[i]),
-                               oldvalue, newvalue))
-           return false;
+       if (rel_is_pk)
+       {
+           /*
+            * If we are looking at the PK table, then do a bytewise
+            * comparison.  We must propagate PK changes if the value is
+            * changed to one that "looks" different but would compare as
+            * equal using the equality operator.  This only makes a
+            * difference for ON UPDATE CASCADE, but for consistency we treat
+            * all changes to the PK the same.
+            */
+           Form_pg_attribute att = TupleDescAttr(oldslot->tts_tupleDescriptor, attnums[i] - 1);
+
+           if (!datum_image_eq(oldvalue, newvalue, att->attbyval, att->attlen))
+               return false;
+       }
+       else
+       {
+           /*
+            * For the FK table, compare with the appropriate equality
+            * operator.  Changes that compare equal will still satisfy the
+            * constraint after the update.
+            */
+           if (!ri_AttributesEqual(riinfo->ff_eq_oprs[i], RIAttType(rel, attnums[i]),
+                                   oldvalue, newvalue))
+               return false;
+       }
    }
 
    return true;
index 5bbf56861031337ce75d8dde59c87683e7fcb124..aa7ec8735c4a104249f659b5b66b5521a9c5b4b0 100644 (file)
@@ -23,6 +23,7 @@
 #include "libpq/pqformat.h"
 #include "miscadmin.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/lsyscache.h"
 #include "utils/typcache.h"
 
@@ -1671,45 +1672,7 @@ record_image_eq(PG_FUNCTION_ARGS)
            }
 
            /* Compare the pair of elements */
-           if (att1->attlen == -1)
-           {
-               Size        len1,
-                           len2;
-
-               len1 = toast_raw_datum_size(values1[i1]);
-               len2 = toast_raw_datum_size(values2[i2]);
-               /* No need to de-toast if lengths don't match. */
-               if (len1 != len2)
-                   result = false;
-               else
-               {
-                   struct varlena *arg1val;
-                   struct varlena *arg2val;
-
-                   arg1val = PG_DETOAST_DATUM_PACKED(values1[i1]);
-                   arg2val = PG_DETOAST_DATUM_PACKED(values2[i2]);
-
-                   result = (memcmp(VARDATA_ANY(arg1val),
-                                    VARDATA_ANY(arg2val),
-                                    len1 - VARHDRSZ) == 0);
-
-                   /* Only free memory if it's a copy made here. */
-                   if ((Pointer) arg1val != (Pointer) values1[i1])
-                       pfree(arg1val);
-                   if ((Pointer) arg2val != (Pointer) values2[i2])
-                       pfree(arg2val);
-               }
-           }
-           else if (att1->attbyval)
-           {
-               result = (values1[i1] == values2[i2]);
-           }
-           else
-           {
-               result = (memcmp(DatumGetPointer(values1[i1]),
-                                DatumGetPointer(values2[i2]),
-                                att1->attlen) == 0);
-           }
+           result = datum_image_eq(values1[i1], values2[i2], att1->attbyval, att2->attlen);
            if (!result)
                break;
        }
index 7b913578ab6da761f34b3a7e8db6038ac6db1b9b..4365bf06e6ae8a7e052455577495d9fbf6dbeaef 100644 (file)
@@ -46,6 +46,15 @@ extern Datum datumTransfer(Datum value, bool typByVal, int typLen);
 extern bool datumIsEqual(Datum value1, Datum value2,
             bool typByVal, int typLen);
 
+/*
+ * datum_image_eq
+ *
+ * Compares two datums for identical contents, based on byte images.  Return
+ * true if the two datums are equal, false otherwise.
+ */
+extern bool datum_image_eq(Datum value1, Datum value2,
+                          bool typByVal, int typLen);
+
 /*
  * Serialize and restore datums so that we can transfer them to parallel
  * workers.
index f1a664e339412acb8e49078a02077e57f75a90d7..401514a3e0075dec7410dff6f6b6b1182881dff1 100644 (file)
@@ -1494,6 +1494,40 @@ delete from pktable2 where f1 = 1;
 alter table fktable2 drop constraint fktable2_f1_fkey;
 ERROR:  cannot ALTER TABLE "pktable2" because it has pending trigger events
 commit;
+drop table pktable2, fktable2;
+--
+-- Test keys that "look" different but compare as equal
+--
+create table pktable2 (a float8, b float8, primary key (a, b));
+create table fktable2 (x float8, y float8, foreign key (x, y) references pktable2 (a, b) on update cascade);
+insert into pktable2 values ('-0', '-0');
+insert into fktable2 values ('-0', '-0');
+select * from pktable2;
+ a  | b  
+----+----
+ -0 | -0
+(1 row)
+
+select * from fktable2;
+ x  | y  
+----+----
+ -0 | -0
+(1 row)
+
+update pktable2 set a = '0' where a = '-0';
+select * from pktable2;
+ a | b  
+---+----
+ 0 | -0
+(1 row)
+
+-- should have updated fktable2.x
+select * from fktable2;
+ x | y  
+---+----
+ 0 | -0
+(1 row)
+
 drop table pktable2, fktable2;
 --
 -- Foreign keys and partitioned tables
index 4639fb45093fde00b58da1fcc8ccf1d5a2a5d720..beeaf3277d36a99d19eb09747c43f0bc9a5a3dcc 100644 (file)
@@ -1120,6 +1120,26 @@ commit;
 
 drop table pktable2, fktable2;
 
+--
+-- Test keys that "look" different but compare as equal
+--
+create table pktable2 (a float8, b float8, primary key (a, b));
+create table fktable2 (x float8, y float8, foreign key (x, y) references pktable2 (a, b) on update cascade);
+
+insert into pktable2 values ('-0', '-0');
+insert into fktable2 values ('-0', '-0');
+
+select * from pktable2;
+select * from fktable2;
+
+update pktable2 set a = '0' where a = '-0';
+
+select * from pktable2;
+-- should have updated fktable2.x
+select * from fktable2;
+
+drop table pktable2, fktable2;
+
 
 --
 -- Foreign keys and partitioned tables