postgres.git / summary / log / commit / refs

commit    fb3a77fd9db03610c5af219d9ddfb499d77df505
Author:   Tom Lane <tgl@sss.pgh.pa.us>
Date:     Sat Apr 05 00:11:48 2025 +0000

    Repair misbehavior with duplicate entries in FK SET column lists.
    
    Since v15 we've had an option to apply a foreign key constraint's
    ON DELETE SET DEFAULT or SET NULL action to just some of the
    referencing columns.  There was not a check for duplicate entries in
    the list of columns-to-set, though.  That caused a potential memory
    stomp in CreateConstraintEntry(), which incautiously assumed that
    the list of columns-to-set couldn't be longer than the number of key
    columns.  Even after fixing that, the case doesn't work because you
    get an error like "multiple assignments to same column" from the SQL
    command that is generated to do the update.
    
    We could either raise an error for duplicate columns or silently
    suppress the dups, and after a bit of thought I chose to do the
    latter.  This is motivated by the fact that duplicates in the FK
    column list are legal, so it's not real clear why duplicates
    in the columns-to-set list shouldn't be.  Of course there's no
    need to actually set the column more than once.
    
    I left in the fix in CreateConstraintEntry() too, just because
    it didn't seem like such low-level code ought to be making
    assumptions about what it's handed.
    
    Bug: #18879
    Reported-by: Yu Liang <luy70@psu.edu>
    Author: Tom Lane <tgl@sss.pgh.pa.us>
    Discussion: https://postgr.es/m/18879-259fc59d072bd4d7@postgresql.org
    Backpatch-through: 15


src/backend/catalog/pg_constraint.c | 3 ++- src/backend/commands/tablecmds.c | 35 ++++++++++++++++++++++++------- src/test/regress/expected/foreign_key.out | 3 ++- src/test/regress/sql/foreign_key.sql | 3 ++- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c index 4002317f705..59565b9d881 100644 --- a/src/backend/catalog/pg_constraint.c +++ b/src/backend/catalog/pg_constraint.c @@ -120,8 +120,9 @@ CreateConstraintEntry(const char *constraintName, if (foreignNKeys > 0) { Datum *fkdatums; + int nkeys = Max(foreignNKeys, numFkDeleteSetCols); - fkdatums = (Datum *) palloc(foreignNKeys * sizeof(Datum)); + fkdatums = (Datum *) palloc(nkeys * sizeof(Datum)); for (i = 0; i < foreignNKeys; i++) fkdatums[i] = Int16GetDatum(foreignKey[i]); confkeyArray = construct_array_builtin(fkdatums, foreignNKeys, INT2OID); diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 40fe7685239..b4c0a615873 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -498,8 +498,8 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo * Relation rel, Constraint *fkconstraint, bool recurse, bool recursing, LOCKMODE lockmode); -static void validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums, - int numfksetcols, const int16 *fksetcolsattnums, +static int validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums, + int numfksetcols, int16 *fksetcolsattnums, List *fksetcols); static ObjectAddress addFkConstraint(addFkConstraintSides fkside, char *constraintname, @@ -9340,9 +9340,10 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, numfkdelsetcols = transformColumnNameList(RelationGetRelid(rel), fkconstraint->fk_del_set_cols, fkdelsetcols, NULL); - validateFkOnDeleteSetColumns(numfks, fkattnum, - numfkdelsetcols, fkdelsetcols, - fkconstraint->fk_del_set_cols); + numfkdelsetcols = validateFkOnDeleteSetColumns(numfks, fkattnum, + numfkdelsetcols, + fkdelsetcols, + fkconstraint->fk_del_set_cols); /* * If the attribute list for the referenced table was omitted, lookup the @@ -9663,17 +9664,23 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, * validateFkOnDeleteSetColumns * Verifies that columns used in ON DELETE SET NULL/DEFAULT (...) * column lists are valid. + * + * If there are duplicates in the fksetcolsattnums[] array, this silently + * removes the dups. The new count of numfksetcols is returned. */ -void +static int validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums, - int numfksetcols, const int16 *fksetcolsattnums, + int numfksetcols, int16 *fksetcolsattnums, List *fksetcols) { + int numcolsout = 0; + for (int i = 0; i < numfksetcols; i++) { int16 setcol_attnum = fksetcolsattnums[i]; bool seen = false; + /* Make sure it's in fkattnums[] */ for (int j = 0; j < numfks; j++) { if (fkattnums[j] == setcol_attnum) @@ -9691,7 +9698,21 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), errmsg("column \"%s\" referenced in ON DELETE SET action must be part of foreign key", col))); } + + /* Now check for dups */ + seen = false; + for (int j = 0; j < numcolsout; j++) + { + if (fksetcolsattnums[j] == setcol_attnum) + { + seen = true; + break; + } + } + if (!seen) + fksetcolsattnums[numcolsout++] = setcol_attnum; } + return numcolsout; } /* diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index 9f3cd3e1d32..19398fe2b4a 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -772,7 +772,8 @@ CREATE TABLE FKTABLE ( fk_id_del_set_null int, fk_id_del_set_default int DEFAULT 0, FOREIGN KEY (tid, fk_id_del_set_null) REFERENCES PKTABLE ON DELETE SET NULL (fk_id_del_set_null), - FOREIGN KEY (tid, fk_id_del_set_default) REFERENCES PKTABLE ON DELETE SET DEFAULT (fk_id_del_set_default) + -- this tests handling of duplicate entries in SET DEFAULT column list + FOREIGN KEY (tid, fk_id_del_set_default) REFERENCES PKTABLE ON DELETE SET DEFAULT (fk_id_del_set_default, fk_id_del_set_default) ); SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'fktable'::regclass::oid ORDER BY oid; pg_get_constraintdef diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql index 3078f7d45c2..1b0c04aa8fe 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -473,7 +473,8 @@ CREATE TABLE FKTABLE ( fk_id_del_set_null int, fk_id_del_set_default int DEFAULT 0, FOREIGN KEY (tid, fk_id_del_set_null) REFERENCES PKTABLE ON DELETE SET NULL (fk_id_del_set_null), - FOREIGN KEY (tid, fk_id_del_set_default) REFERENCES PKTABLE ON DELETE SET DEFAULT (fk_id_del_set_default) + -- this tests handling of duplicate entries in SET DEFAULT column list + FOREIGN KEY (tid, fk_id_del_set_default) REFERENCES PKTABLE ON DELETE SET DEFAULT (fk_id_del_set_default, fk_id_del_set_default) ); SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'fktable'::regclass::oid ORDER BY oid; [parent: 41932139882e]