Skip to content

Commit 1ffa59a

Browse files
committed
Fix optimization of foreign-key on update actions
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/[email protected]
1 parent fb58065 commit 1ffa59a

File tree

6 files changed

+149
-52
lines changed

6 files changed

+149
-52
lines changed

src/backend/utils/adt/datum.c

+57
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242

4343
#include "postgres.h"
4444

45+
#include "access/tuptoaster.h"
46+
#include "fmgr.h"
4547
#include "utils/datum.h"
4648
#include "utils/expandeddatum.h"
4749

@@ -251,6 +253,61 @@ datumIsEqual(Datum value1, Datum value2, bool typByVal, int typLen)
251253
return res;
252254
}
253255

256+
/*-------------------------------------------------------------------------
257+
* datum_image_eq
258+
*
259+
* Compares two datums for identical contents, based on byte images. Return
260+
* true if the two datums are equal, false otherwise.
261+
*-------------------------------------------------------------------------
262+
*/
263+
bool
264+
datum_image_eq(Datum value1, Datum value2, bool typByVal, int typLen)
265+
{
266+
bool result = true;
267+
268+
if (typLen == -1)
269+
{
270+
Size len1,
271+
len2;
272+
273+
len1 = toast_raw_datum_size(value1);
274+
len2 = toast_raw_datum_size(value2);
275+
/* No need to de-toast if lengths don't match. */
276+
if (len1 != len2)
277+
result = false;
278+
else
279+
{
280+
struct varlena *arg1val;
281+
struct varlena *arg2val;
282+
283+
arg1val = PG_DETOAST_DATUM_PACKED(value1);
284+
arg2val = PG_DETOAST_DATUM_PACKED(value2);
285+
286+
result = (memcmp(VARDATA_ANY(arg1val),
287+
VARDATA_ANY(arg2val),
288+
len1 - VARHDRSZ) == 0);
289+
290+
/* Only free memory if it's a copy made here. */
291+
if ((Pointer) arg1val != (Pointer) value1)
292+
pfree(arg1val);
293+
if ((Pointer) arg2val != (Pointer) value2)
294+
pfree(arg2val);
295+
}
296+
}
297+
else if (typByVal)
298+
{
299+
result = (value1 == value2);
300+
}
301+
else
302+
{
303+
result = (memcmp(DatumGetPointer(value1),
304+
DatumGetPointer(value2),
305+
typLen) == 0);
306+
}
307+
308+
return result;
309+
}
310+
254311
/*-------------------------------------------------------------------------
255312
* datumEstimateSpace
256313
*

src/backend/utils/adt/ri_triggers.c

+27-13
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
#include "storage/bufmgr.h"
4343
#include "utils/acl.h"
4444
#include "utils/builtins.h"
45+
#include "utils/datum.h"
4546
#include "utils/fmgroids.h"
4647
#include "utils/guc.h"
4748
#include "utils/inval.h"
@@ -2402,18 +2403,11 @@ ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
24022403
const RI_ConstraintInfo *riinfo, bool rel_is_pk)
24032404
{
24042405
const int16 *attnums;
2405-
const Oid *eq_oprs;
24062406

24072407
if (rel_is_pk)
2408-
{
24092408
attnums = riinfo->pk_attnums;
2410-
eq_oprs = riinfo->pp_eq_oprs;
2411-
}
24122409
else
2413-
{
24142410
attnums = riinfo->fk_attnums;
2415-
eq_oprs = riinfo->ff_eq_oprs;
2416-
}
24172411

24182412
/* XXX: could be worthwhile to fetch all necessary attrs at once */
24192413
for (int i = 0; i < riinfo->nkeys; i++)
@@ -2436,12 +2430,32 @@ ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
24362430
if (isnull)
24372431
return false;
24382432

2439-
/*
2440-
* Compare them with the appropriate equality operator.
2441-
*/
2442-
if (!ri_AttributesEqual(eq_oprs[i], RIAttType(rel, attnums[i]),
2443-
oldvalue, newvalue))
2444-
return false;
2433+
if (rel_is_pk)
2434+
{
2435+
/*
2436+
* If we are looking at the PK table, then do a bytewise
2437+
* comparison. We must propagate PK changes if the value is
2438+
* changed to one that "looks" different but would compare as
2439+
* equal using the equality operator. This only makes a
2440+
* difference for ON UPDATE CASCADE, but for consistency we treat
2441+
* all changes to the PK the same.
2442+
*/
2443+
Form_pg_attribute att = TupleDescAttr(oldslot->tts_tupleDescriptor, attnums[i] - 1);
2444+
2445+
if (!datum_image_eq(oldvalue, newvalue, att->attbyval, att->attlen))
2446+
return false;
2447+
}
2448+
else
2449+
{
2450+
/*
2451+
* For the FK table, compare with the appropriate equality
2452+
* operator. Changes that compare equal will still satisfy the
2453+
* constraint after the update.
2454+
*/
2455+
if (!ri_AttributesEqual(riinfo->ff_eq_oprs[i], RIAttType(rel, attnums[i]),
2456+
oldvalue, newvalue))
2457+
return false;
2458+
}
24452459
}
24462460

24472461
return true;

src/backend/utils/adt/rowtypes.c

+2-39
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "libpq/pqformat.h"
2424
#include "miscadmin.h"
2525
#include "utils/builtins.h"
26+
#include "utils/datum.h"
2627
#include "utils/lsyscache.h"
2728
#include "utils/typcache.h"
2829

@@ -1671,45 +1672,7 @@ record_image_eq(PG_FUNCTION_ARGS)
16711672
}
16721673

16731674
/* Compare the pair of elements */
1674-
if (att1->attlen == -1)
1675-
{
1676-
Size len1,
1677-
len2;
1678-
1679-
len1 = toast_raw_datum_size(values1[i1]);
1680-
len2 = toast_raw_datum_size(values2[i2]);
1681-
/* No need to de-toast if lengths don't match. */
1682-
if (len1 != len2)
1683-
result = false;
1684-
else
1685-
{
1686-
struct varlena *arg1val;
1687-
struct varlena *arg2val;
1688-
1689-
arg1val = PG_DETOAST_DATUM_PACKED(values1[i1]);
1690-
arg2val = PG_DETOAST_DATUM_PACKED(values2[i2]);
1691-
1692-
result = (memcmp(VARDATA_ANY(arg1val),
1693-
VARDATA_ANY(arg2val),
1694-
len1 - VARHDRSZ) == 0);
1695-
1696-
/* Only free memory if it's a copy made here. */
1697-
if ((Pointer) arg1val != (Pointer) values1[i1])
1698-
pfree(arg1val);
1699-
if ((Pointer) arg2val != (Pointer) values2[i2])
1700-
pfree(arg2val);
1701-
}
1702-
}
1703-
else if (att1->attbyval)
1704-
{
1705-
result = (values1[i1] == values2[i2]);
1706-
}
1707-
else
1708-
{
1709-
result = (memcmp(DatumGetPointer(values1[i1]),
1710-
DatumGetPointer(values2[i2]),
1711-
att1->attlen) == 0);
1712-
}
1675+
result = datum_image_eq(values1[i1], values2[i2], att1->attbyval, att2->attlen);
17131676
if (!result)
17141677
break;
17151678
}

src/include/utils/datum.h

+9
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ extern Datum datumTransfer(Datum value, bool typByVal, int typLen);
4646
extern bool datumIsEqual(Datum value1, Datum value2,
4747
bool typByVal, int typLen);
4848

49+
/*
50+
* datum_image_eq
51+
*
52+
* Compares two datums for identical contents, based on byte images. Return
53+
* true if the two datums are equal, false otherwise.
54+
*/
55+
extern bool datum_image_eq(Datum value1, Datum value2,
56+
bool typByVal, int typLen);
57+
4958
/*
5059
* Serialize and restore datums so that we can transfer them to parallel
5160
* workers.

src/test/regress/expected/foreign_key.out

+34
Original file line numberDiff line numberDiff line change
@@ -1494,6 +1494,40 @@ delete from pktable2 where f1 = 1;
14941494
alter table fktable2 drop constraint fktable2_f1_fkey;
14951495
ERROR: cannot ALTER TABLE "pktable2" because it has pending trigger events
14961496
commit;
1497+
drop table pktable2, fktable2;
1498+
--
1499+
-- Test keys that "look" different but compare as equal
1500+
--
1501+
create table pktable2 (a float8, b float8, primary key (a, b));
1502+
create table fktable2 (x float8, y float8, foreign key (x, y) references pktable2 (a, b) on update cascade);
1503+
insert into pktable2 values ('-0', '-0');
1504+
insert into fktable2 values ('-0', '-0');
1505+
select * from pktable2;
1506+
a | b
1507+
----+----
1508+
-0 | -0
1509+
(1 row)
1510+
1511+
select * from fktable2;
1512+
x | y
1513+
----+----
1514+
-0 | -0
1515+
(1 row)
1516+
1517+
update pktable2 set a = '0' where a = '-0';
1518+
select * from pktable2;
1519+
a | b
1520+
---+----
1521+
0 | -0
1522+
(1 row)
1523+
1524+
-- should have updated fktable2.x
1525+
select * from fktable2;
1526+
x | y
1527+
---+----
1528+
0 | -0
1529+
(1 row)
1530+
14971531
drop table pktable2, fktable2;
14981532
--
14991533
-- Foreign keys and partitioned tables

src/test/regress/sql/foreign_key.sql

+20
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,26 @@ commit;
11201120

11211121
drop table pktable2, fktable2;
11221122

1123+
--
1124+
-- Test keys that "look" different but compare as equal
1125+
--
1126+
create table pktable2 (a float8, b float8, primary key (a, b));
1127+
create table fktable2 (x float8, y float8, foreign key (x, y) references pktable2 (a, b) on update cascade);
1128+
1129+
insert into pktable2 values ('-0', '-0');
1130+
insert into fktable2 values ('-0', '-0');
1131+
1132+
select * from pktable2;
1133+
select * from fktable2;
1134+
1135+
update pktable2 set a = '0' where a = '-0';
1136+
1137+
select * from pktable2;
1138+
-- should have updated fktable2.x
1139+
select * from fktable2;
1140+
1141+
drop table pktable2, fktable2;
1142+
11231143

11241144
--
11251145
-- Foreign keys and partitioned tables

0 commit comments

Comments
 (0)