diff options
| author | vvvv <[email protected]> | 2025-10-24 14:59:50 +0300 |
|---|---|---|
| committer | vvvv <[email protected]> | 2025-10-24 15:29:24 +0300 |
| commit | 5b0d18921f2a509d8363c40a5ca208dfed026287 (patch) | |
| tree | d1369c696d3a9e9a65b68d9208e198269a48cfbc /yql/essentials/parser/pg_wrapper/postgresql/src/backend/commands/tablecmds.c | |
| parent | e7fbdb6e81ae4a296e710b133de7a2a04b31bbc4 (diff) | |
YQL-20567 upgrade PG up to 16.10 & fix instructions
init
commit_hash:81aba13295273281d19d2d332a48ff1c44977447
Diffstat (limited to 'yql/essentials/parser/pg_wrapper/postgresql/src/backend/commands/tablecmds.c')
| -rw-r--r-- | yql/essentials/parser/pg_wrapper/postgresql/src/backend/commands/tablecmds.c | 1033 |
1 files changed, 690 insertions, 343 deletions
diff --git a/yql/essentials/parser/pg_wrapper/postgresql/src/backend/commands/tablecmds.c b/yql/essentials/parser/pg_wrapper/postgresql/src/backend/commands/tablecmds.c index f84348dc6ca..0b212e623e3 100644 --- a/yql/essentials/parser/pg_wrapper/postgresql/src/backend/commands/tablecmds.c +++ b/yql/essentials/parser/pg_wrapper/postgresql/src/backend/commands/tablecmds.c @@ -336,6 +336,14 @@ typedef struct ForeignTruncateInfo List *rels; } ForeignTruncateInfo; +/* Partial or complete FK creation in addFkConstraint() */ +typedef enum addFkConstraintSides +{ + addFkReferencedSide, + addFkReferencingSide, + addFkBothSides, +} addFkConstraintSides; + /* * Partition tables are expected to be dropped when the parent partitioned * table gets dropped. Hence for partitioning we use AUTO dependency. @@ -388,6 +396,7 @@ static CoercionPathType findFkeyCast(Oid targetTypeId, Oid sourceTypeId, static void validateForeignKeyConstraint(char *conname, Relation rel, Relation pkrel, Oid pkindOid, Oid constraintOid); +static void CheckAlterTableIsSafe(Relation rel); static void ATController(AlterTableStmt *parsetree, Relation rel, List *cmds, bool recurse, LOCKMODE lockmode, AlterTableUtilityContext *context); @@ -489,16 +498,25 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo * Relation rel, Constraint *fkconstraint, bool recurse, bool recursing, LOCKMODE lockmode); -static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, - Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr, - int numfks, int16 *pkattnum, int16 *fkattnum, - Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators, - int numfkdelsetcols, int16 *fkdelsetcols, - bool old_check_ok, - Oid parentDelTrigger, Oid parentUpdTrigger); -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, + Constraint *fkconstraint, Relation rel, + Relation pkrel, Oid indexOid, + Oid parentConstr, + int numfks, int16 *pkattnum, int16 *fkattnum, + Oid *pfeqoperators, Oid *ppeqoperators, + Oid *ffeqoperators, int numfkdelsetcols, + int16 *fkdelsetcols, bool is_internal); +static void addFkRecurseReferenced(Constraint *fkconstraint, + Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr, + int numfks, int16 *pkattnum, int16 *fkattnum, + Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators, + int numfkdelsetcols, int16 *fkdelsetcols, + bool old_check_ok, + Oid parentDelTrigger, Oid parentUpdTrigger); static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr, int numfks, int16 *pkattnum, int16 *fkattnum, @@ -3324,8 +3342,15 @@ findAttrByName(const char *attributeName, List *schema) * SetRelationHasSubclass * Set the value of the relation's relhassubclass field in pg_class. * - * NOTE: caller must be holding an appropriate lock on the relation. - * ShareUpdateExclusiveLock is sufficient. + * It's always safe to set this field to true, because all SQL commands are + * ready to see true and then find no children. On the other hand, commands + * generally assume zero children if this is false. + * + * Caller must hold any self-exclusive lock until end of transaction. If the + * new value is false, caller must have acquired that lock before reading the + * evidence that justified the false value. That way, it properly waits if + * another backend is simultaneously concluding no need to change the tuple + * (new and old values are true). * * NOTE: an important side-effect of this operation is that an SI invalidation * message is sent out to all backends --- including me --- causing plans @@ -3340,6 +3365,11 @@ SetRelationHasSubclass(Oid relationId, bool relhassubclass) HeapTuple tuple; Form_pg_class classtuple; + Assert(CheckRelationOidLockedByMe(relationId, + ShareUpdateExclusiveLock, false) || + CheckRelationOidLockedByMe(relationId, + ShareRowExclusiveLock, true)); + /* * Fetch a modifiable copy of the tuple, modify it, update pg_class. */ @@ -3438,6 +3468,7 @@ SetRelationTableSpace(Relation rel, { Relation pg_class; HeapTuple tuple; + ItemPointerData otid; Form_pg_class rd_rel; Oid reloid = RelationGetRelid(rel); @@ -3446,9 +3477,10 @@ SetRelationTableSpace(Relation rel, /* Get a modifiable copy of the relation's pg_class row. */ pg_class = table_open(RelationRelationId, RowExclusiveLock); - tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid)); + tuple = SearchSysCacheLockedCopy1(RELOID, ObjectIdGetDatum(reloid)); if (!HeapTupleIsValid(tuple)) elog(ERROR, "cache lookup failed for relation %u", reloid); + otid = tuple->t_self; rd_rel = (Form_pg_class) GETSTRUCT(tuple); /* Update the pg_class row. */ @@ -3456,7 +3488,8 @@ SetRelationTableSpace(Relation rel, InvalidOid : newTableSpaceId; if (RelFileNumberIsValid(newRelFilenumber)) rd_rel->relfilenode = newRelFilenumber; - CatalogTupleUpdate(pg_class, &tuple->t_self, tuple); + CatalogTupleUpdate(pg_class, &otid, tuple); + UnlockTuple(pg_class, &otid, InplaceUpdateTupleLock); /* * Record dependency on tablespace. This is only required for relations @@ -3950,6 +3983,7 @@ RenameRelationInternal(Oid myrelid, const char *newrelname, bool is_internal, bo { Relation targetrelation; Relation relrelation; /* for RELATION relation */ + ItemPointerData otid; HeapTuple reltup; Form_pg_class relform; Oid namespaceId; @@ -3972,9 +4006,10 @@ RenameRelationInternal(Oid myrelid, const char *newrelname, bool is_internal, bo */ relrelation = table_open(RelationRelationId, RowExclusiveLock); - reltup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(myrelid)); + reltup = SearchSysCacheLockedCopy1(RELOID, ObjectIdGetDatum(myrelid)); if (!HeapTupleIsValid(reltup)) /* shouldn't happen */ elog(ERROR, "cache lookup failed for relation %u", myrelid); + otid = reltup->t_self; relform = (Form_pg_class) GETSTRUCT(reltup); if (get_relname_relid(newrelname, namespaceId) != InvalidOid) @@ -3999,7 +4034,8 @@ RenameRelationInternal(Oid myrelid, const char *newrelname, bool is_internal, bo */ namestrcpy(&(relform->relname), newrelname); - CatalogTupleUpdate(relrelation, &reltup->t_self, reltup); + CatalogTupleUpdate(relrelation, &otid, reltup); + UnlockTuple(relrelation, &otid, InplaceUpdateTupleLock); InvokeObjectPostAlterHookArg(RelationRelationId, myrelid, 0, InvalidOid, is_internal); @@ -4112,6 +4148,37 @@ CheckTableNotInUse(Relation rel, const char *stmt) } /* + * CheckAlterTableIsSafe + * Verify that it's safe to allow ALTER TABLE on this relation. + * + * This consists of CheckTableNotInUse() plus a check that the relation + * isn't another session's temp table. We must split out the temp-table + * check because there are callers of CheckTableNotInUse() that don't want + * that, notably DROP TABLE. (We must allow DROP or we couldn't clean out + * an orphaned temp schema.) Compare truncate_check_activity(). + */ +static void +CheckAlterTableIsSafe(Relation rel) +{ + /* + * Don't allow ALTER on temp tables of other backends. Their local buffer + * manager is not going to cope if we need to change the table's contents. + * Even if we don't, there may be optimizations that assume temp tables + * aren't subject to such interference. + */ + if (RELATION_IS_OTHER_TEMP(rel)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter temporary tables of other sessions"))); + + /* + * Also check for active uses of the relation in the current transaction, + * including open scans and pending AFTER trigger events. + */ + CheckTableNotInUse(rel, "ALTER TABLE"); +} + +/* * AlterTableLookupRelation * Look up, and lock, the OID for the relation named by an alter table * statement. @@ -4184,7 +4251,7 @@ AlterTable(AlterTableStmt *stmt, LOCKMODE lockmode, /* Caller is required to provide an adequate lock. */ rel = relation_open(context->relid, NoLock); - CheckTableNotInUse(rel, "ALTER TABLE"); + CheckAlterTableIsSafe(rel); ATController(stmt, rel, stmt->cmds, stmt->relation->inh, lockmode, context); } @@ -5541,7 +5608,9 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode, /* * Don't allow rewrite on temp tables of other backends ... their - * local buffer manager is not going to cope. + * local buffer manager is not going to cope. (This is redundant + * with the check in CheckAlterTableIsSafe, but for safety we'll + * check here too.) */ if (RELATION_IS_OTHER_TEMP(OldHeap)) ereport(ERROR, @@ -6405,7 +6474,7 @@ ATSimpleRecursion(List **wqueue, Relation rel, continue; /* find_all_inheritors already got lock */ childrel = relation_open(childrelid, NoLock); - CheckTableNotInUse(childrel, "ALTER TABLE"); + CheckAlterTableIsSafe(childrel); ATPrepCmd(wqueue, childrel, cmd, false, true, lockmode, context); relation_close(childrel, NoLock); } @@ -6414,7 +6483,7 @@ ATSimpleRecursion(List **wqueue, Relation rel, /* * Obtain list of partitions of the given table, locking them all at the given - * lockmode and ensuring that they all pass CheckTableNotInUse. + * lockmode and ensuring that they all pass CheckAlterTableIsSafe. * * This function is a no-op if the given relation is not a partitioned table; * in particular, nothing is done if it's a legacy inheritance parent. @@ -6435,7 +6504,7 @@ ATCheckPartitionsNotInUse(Relation rel, LOCKMODE lockmode) /* find_all_inheritors already got lock */ childrel = table_open(lfirst_oid(cell), NoLock); - CheckTableNotInUse(childrel, "ALTER TABLE"); + CheckAlterTableIsSafe(childrel); table_close(childrel, NoLock); } list_free(inh); @@ -6468,7 +6537,7 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, Relation childrel; childrel = relation_open(childrelid, lockmode); - CheckTableNotInUse(childrel, "ALTER TABLE"); + CheckAlterTableIsSafe(childrel); ATPrepCmd(wqueue, childrel, cmd, true, true, lockmode, context); relation_close(childrel, NoLock); } @@ -7009,14 +7078,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, rawEnt = (RawColumnDefault *) palloc(sizeof(RawColumnDefault)); rawEnt->attnum = attribute.attnum; rawEnt->raw_default = copyObject(colDef->raw_default); - - /* - * Attempt to skip a complete table rewrite by storing the specified - * DEFAULT value outside of the heap. This may be disabled inside - * AddRelationNewConstraints if the optimization cannot be applied. - */ - rawEnt->missingMode = (!colDef->generated); - + rawEnt->missingMode = false; /* XXX vestigial */ rawEnt->generated = colDef->generated; /* @@ -7028,13 +7090,6 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, /* Make the additional catalog changes visible */ CommandCounterIncrement(); - - /* - * Did the request for a missing value work? If not we'll have to do a - * rewrite - */ - if (!rawEnt->missingMode) - tab->rewrite |= AT_REWRITE_DEFAULT_VAL; } /* @@ -7051,9 +7106,9 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, * rejects nulls. If there are any domain constraints then we construct * an explicit NULL default value that will be passed through * CoerceToDomain processing. (This is a tad inefficient, since it causes - * rewriting the table which we really don't have to do, but the present - * design of domain processing doesn't offer any simple way of checking - * the constraints more directly.) + * rewriting the table which we really wouldn't have to do; but we do it + * to preserve the historical behavior that such a failure will be raised + * only if the table currently contains some rows.) * * Note: we use build_column_default, and not just the cooked default * returned by AddRelationNewConstraints, so that the right thing happens @@ -7072,6 +7127,9 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, */ if (RELKIND_HAS_STORAGE(relkind) && attribute.attnum > 0) { + bool has_domain_constraints; + bool has_missing = false; + /* * For an identity column, we can't use build_column_default(), * because the sequence ownership isn't set yet. So do it manually. @@ -7084,14 +7142,13 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, nve->typeId = typeOid; defval = (Expr *) nve; - - /* must do a rewrite for identity columns */ - tab->rewrite |= AT_REWRITE_DEFAULT_VAL; } else defval = (Expr *) build_column_default(rel, attribute.attnum); - if (!defval && DomainHasConstraints(typeOid)) + /* Build CoerceToDomain(NULL) expression if needed */ + has_domain_constraints = DomainHasConstraints(typeOid); + if (!defval && has_domain_constraints) { Oid baseTypeId; int32 baseTypeMod; @@ -7117,18 +7174,63 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, { NewColumnValue *newval; + /* Prepare defval for execution, either here or in Phase 3 */ + defval = expression_planner(defval); + + /* Add the new default to the newvals list */ newval = (NewColumnValue *) palloc0(sizeof(NewColumnValue)); newval->attnum = attribute.attnum; - newval->expr = expression_planner(defval); + newval->expr = defval; newval->is_generated = (colDef->generated != '\0'); tab->newvals = lappend(tab->newvals, newval); - } - if (DomainHasConstraints(typeOid)) - tab->rewrite |= AT_REWRITE_DEFAULT_VAL; + /* + * Attempt to skip a complete table rewrite by storing the + * specified DEFAULT value outside of the heap. This is only + * allowed for plain relations and non-generated columns, and the + * default expression can't be volatile (stable is OK). Note that + * contain_volatile_functions deems CoerceToDomain immutable, but + * here we consider that coercion to a domain with constraints is + * volatile; else it might fail even when the table is empty. + */ + if (rel->rd_rel->relkind == RELKIND_RELATION && + !colDef->generated && + !has_domain_constraints && + !contain_volatile_functions((Node *) defval)) + { + EState *estate; + ExprState *exprState; + Datum missingval; + bool missingIsNull; + + /* Evaluate the default expression */ + estate = CreateExecutorState(); + exprState = ExecPrepareExpr(defval, estate); + missingval = ExecEvalExpr(exprState, + GetPerTupleExprContext(estate), + &missingIsNull); + /* If it turns out NULL, nothing to do; else store it */ + if (!missingIsNull) + { + StoreAttrMissingVal(rel, attribute.attnum, missingval); + /* Make the additional catalog change visible */ + CommandCounterIncrement(); + has_missing = true; + } + FreeExecutorState(estate); + } + else + { + /* + * Failed to use missing mode. We have to do a table rewrite + * to install the value. + */ + tab->rewrite |= AT_REWRITE_DEFAULT_VAL; + } + } - if (!TupleDescAttr(rel->rd_att, attribute.attnum - 1)->atthasmissing) + if (!has_missing) { /* * If the new column is NOT NULL, and there is no missing value, @@ -7180,7 +7282,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, /* find_inheritance_children already got lock */ childrel = table_open(childrelid, NoLock); - CheckTableNotInUse(childrel, "ALTER TABLE"); + CheckAlterTableIsSafe(childrel); /* Find or create work queue entry for this table */ childtab = ATGetQueueEntry(wqueue, childrel); @@ -8612,7 +8714,7 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName, /* find_inheritance_children already got lock */ childrel = table_open(childrelid, NoLock); - CheckTableNotInUse(childrel, "ALTER TABLE"); + CheckAlterTableIsSafe(childrel); tuple = SearchSysCacheCopyAttName(childrelid, colName); if (!HeapTupleIsValid(tuple)) /* shouldn't happen */ @@ -9095,7 +9197,7 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, /* find_inheritance_children already got lock */ childrel = table_open(childrelid, NoLock); - CheckTableNotInUse(childrel, "ALTER TABLE"); + CheckAlterTableIsSafe(childrel); /* Find or create work queue entry for this table */ childtab = ATGetQueueEntry(wqueue, childrel); @@ -9238,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 @@ -9503,25 +9606,37 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, ffeqoperators[i] = ffeqop; } - /* - * Create all the constraint and trigger objects, recursing to partitions - * as necessary. First handle the referenced side. - */ - address = addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel, - indexOid, - InvalidOid, /* no parent constraint */ - numfks, - pkattnum, - fkattnum, - pfeqoperators, - ppeqoperators, - ffeqoperators, - numfkdelsetcols, - fkdelsetcols, - old_check_ok, - InvalidOid, InvalidOid); - - /* Now handle the referencing side. */ + /* First, create the constraint catalog entry itself. */ + address = addFkConstraint(addFkBothSides, + fkconstraint->conname, fkconstraint, rel, pkrel, + indexOid, + InvalidOid, /* no parent constraint */ + numfks, + pkattnum, + fkattnum, + pfeqoperators, + ppeqoperators, + ffeqoperators, + numfkdelsetcols, + fkdelsetcols, + false); + + /* Next process the action triggers at the referenced side and recurse */ + addFkRecurseReferenced(fkconstraint, rel, pkrel, + indexOid, + address.objectId, + numfks, + pkattnum, + fkattnum, + pfeqoperators, + ppeqoperators, + ffeqoperators, + numfkdelsetcols, + fkdelsetcols, + old_check_ok, + InvalidOid, InvalidOid); + + /* Lastly create the check triggers at the referencing side and recurse */ addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel, indexOid, address.objectId, @@ -9549,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) @@ -9577,50 +9698,59 @@ 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; } /* - * addFkRecurseReferenced - * subroutine for ATAddForeignKeyConstraint; recurses on the referenced - * side of the constraint + * addFkConstraint + * Install pg_constraint entries to implement a foreign key constraint. + * Caller must separately invoke addFkRecurseReferenced and + * addFkRecurseReferencing, as appropriate, to install pg_trigger entries + * and (for partitioned tables) recurse to partitions. * - * Create pg_constraint rows for the referenced side of the constraint, - * referencing the parent of the referencing side; also create action triggers - * on leaf partitions. If the table is partitioned, recurse to handle each - * partition. - * - * wqueue is the ALTER TABLE work queue; can be NULL when not running as part - * of an ALTER TABLE sequence. - * fkconstraint is the constraint being added. - * rel is the root referencing relation. - * pkrel is the referenced relation; might be a partition, if recursing. - * indexOid is the OID of the index (on pkrel) implementing this constraint. - * parentConstr is the OID of a parent constraint; InvalidOid if this is a - * top-level constraint. - * numfks is the number of columns in the foreign key - * pkattnum is the attnum array of referenced attributes. - * fkattnum is the attnum array of referencing attributes. - * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT + * fkside: the side of the FK (or both) to create. Caller should + * call addFkRecurseReferenced if this is addFkReferencedSide, + * addFkRecurseReferencing if it's addFkReferencingSide, or both if it's + * addFkBothSides. + * constraintname: the base name for the constraint being added, + * copied to fkconstraint->conname if the latter is not set + * fkconstraint: the constraint being added + * rel: the root referencing relation + * pkrel: the referenced relation; might be a partition, if recursing + * indexOid: the OID of the index (on pkrel) implementing this constraint + * parentConstr: the OID of a parent constraint; InvalidOid if this is a + * top-level constraint + * numfks: the number of columns in the foreign key + * pkattnum: the attnum array of referenced attributes + * fkattnum: the attnum array of referencing attributes + * pf/pp/ffeqoperators: OID array of operators between columns + * numfkdelsetcols: the number of columns in the ON DELETE SET NULL/DEFAULT * (...) clause - * fkdelsetcols is the attnum array of the columns in the ON DELETE SET + * fkdelsetcols: the attnum array of the columns in the ON DELETE SET * NULL/DEFAULT clause - * pf/pp/ffeqoperators are OID array of operators between columns. - * old_check_ok signals that this constraint replaces an existing one that - * was already validated (thus this one doesn't need validation). - * parentDelTrigger and parentUpdTrigger, when being recursively called on - * a partition, are the OIDs of the parent action triggers for DELETE and - * UPDATE respectively. */ static ObjectAddress -addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, - Relation pkrel, Oid indexOid, Oid parentConstr, - int numfks, - int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators, - Oid *ppeqoperators, Oid *ffeqoperators, - int numfkdelsetcols, int16 *fkdelsetcols, - bool old_check_ok, - Oid parentDelTrigger, Oid parentUpdTrigger) +addFkConstraint(addFkConstraintSides fkside, + char *constraintname, Constraint *fkconstraint, + Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr, + int numfks, int16 *pkattnum, + int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators, + Oid *ffeqoperators, int numfkdelsetcols, int16 *fkdelsetcols, + bool is_internal) { ObjectAddress address; Oid constrOid; @@ -9628,8 +9758,6 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, bool conislocal; int coninhcount; bool connoinherit; - Oid deleteTriggerOid, - updateTriggerOid; /* * Verify relkind for each referenced partition. At the top level, this @@ -9648,13 +9776,16 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, */ if (ConstraintNameIsUsed(CONSTRAINT_RELATION, RelationGetRelid(rel), - fkconstraint->conname)) + constraintname)) conname = ChooseConstraintName(RelationGetRelationName(rel), ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs), "fkey", RelationGetNamespace(rel), NIL); else - conname = fkconstraint->conname; + conname = constraintname; + + if (fkconstraint->conname == NULL) + fkconstraint->conname = pstrdup(conname); if (OidIsValid(parentConstr)) { @@ -9706,33 +9837,107 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, conislocal, /* islocal */ coninhcount, /* inhcount */ connoinherit, /* conNoInherit */ - false); /* is_internal */ + is_internal); /* is_internal */ ObjectAddressSet(address, ConstraintRelationId, constrOid); /* - * Mark the child constraint as part of the parent constraint; it must not - * be dropped on its own. (This constraint is deleted when the partition - * is detached, but a special check needs to occur that the partition - * contains no referenced values.) + * In partitioning cases, create the dependency entries for this + * constraint. (For non-partitioned cases, relevant entries were created + * by CreateConstraintEntry.) + * + * On the referenced side, we need the constraint to have an internal + * dependency on its parent constraint; this means that this constraint + * cannot be dropped on its own -- only through the parent constraint. It + * also means the containing partition cannot be dropped on its own, but + * it can be detached, at which point this dependency is removed (after + * verifying that no rows are referenced via this FK.) + * + * When processing the referencing side, we link the constraint via the + * special partitioning dependencies: the parent constraint is the primary + * dependent, and the partition on which the foreign key exists is the + * secondary dependency. That way, this constraint is dropped if either + * of these objects is. + * + * Note that this is only necessary for the subsidiary pg_constraint rows + * in partitions; the topmost row doesn't need any of this. */ if (OidIsValid(parentConstr)) { ObjectAddress referenced; ObjectAddressSet(referenced, ConstraintRelationId, parentConstr); - recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL); + + Assert(fkside != addFkBothSides); + if (fkside == addFkReferencedSide) + recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL); + else + { + recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI); + ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(rel)); + recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC); + } } /* make new constraint visible, in case we add more */ CommandCounterIncrement(); + return address; +} + +/* + * addFkRecurseReferenced + * Recursive helper for the referenced side of foreign key creation, + * which creates the action triggers and recurses + * + * If the referenced relation is a plain relation, create the necessary action + * triggers that implement the constraint. If the referenced relation is a + * partitioned table, then we create a pg_constraint row referencing the parent + * of the referencing side for it and recurse on this routine for each + * partition. + * + * fkconstraint: the constraint being added + * rel: the root referencing relation + * pkrel: the referenced relation; might be a partition, if recursing + * indexOid: the OID of the index (on pkrel) implementing this constraint + * parentConstr: the OID of a parent constraint; InvalidOid if this is a + * top-level constraint + * numfks: the number of columns in the foreign key + * pkattnum: the attnum array of referenced attributes + * fkattnum: the attnum array of referencing attributes + * numfkdelsetcols: the number of columns in the ON DELETE SET + * NULL/DEFAULT (...) clause + * fkdelsetcols: the attnum array of the columns in the ON DELETE SET + * NULL/DEFAULT clause + * pf/pp/ffeqoperators: OID array of operators between columns + * old_check_ok: true if this constraint replaces an existing one that + * was already validated (thus this one doesn't need validation) + * parentDelTrigger and parentUpdTrigger: when recursively called on a + * partition, the OIDs of the parent action triggers for DELETE and + * UPDATE respectively. + */ +static void +addFkRecurseReferenced(Constraint *fkconstraint, Relation rel, + Relation pkrel, Oid indexOid, Oid parentConstr, + int numfks, + int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators, + Oid *ppeqoperators, Oid *ffeqoperators, + int numfkdelsetcols, int16 *fkdelsetcols, + bool old_check_ok, + Oid parentDelTrigger, Oid parentUpdTrigger) +{ + Oid deleteTriggerOid, + updateTriggerOid; + + Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true)); + Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true)); + /* * Create the action triggers that enforce the constraint. */ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel), fkconstraint, - constrOid, indexOid, + parentConstr, indexOid, parentDelTrigger, parentUpdTrigger, &deleteTriggerOid, &updateTriggerOid); @@ -9751,7 +9956,9 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, AttrMap *map; AttrNumber *mapped_pkattnum; Oid partIndexId; + ObjectAddress address; + /* XXX would it be better to acquire these locks beforehand? */ partRel = table_open(pd->oids[i], ShareRowExclusiveLock); /* @@ -9770,13 +9977,23 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, else mapped_pkattnum = pkattnum; - /* do the deed */ + /* Determine the index to use at this level */ partIndexId = index_get_partition(partRel, indexOid); if (!OidIsValid(partIndexId)) elog(ERROR, "index for %u not found in partition %s", indexOid, RelationGetRelationName(partRel)); - addFkRecurseReferenced(wqueue, fkconstraint, rel, partRel, - partIndexId, constrOid, numfks, + + /* Create entry at this level ... */ + address = addFkConstraint(addFkReferencedSide, + fkconstraint->conname, fkconstraint, rel, + partRel, partIndexId, parentConstr, + numfks, mapped_pkattnum, + fkattnum, pfeqoperators, ppeqoperators, + ffeqoperators, numfkdelsetcols, + fkdelsetcols, true); + /* ... and recurse to our children */ + addFkRecurseReferenced(fkconstraint, rel, partRel, + partIndexId, address.objectId, numfks, mapped_pkattnum, fkattnum, pfeqoperators, ppeqoperators, ffeqoperators, numfkdelsetcols, fkdelsetcols, @@ -9792,13 +10009,12 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, } } } - - return address; } /* * addFkRecurseReferencing - * subroutine for ATAddForeignKeyConstraint and CloneFkReferencing + * Recursive helper for the referencing side of foreign key creation, + * which creates the check triggers and recurses * * If the referencing relation is a plain relation, create the necessary check * triggers that implement the constraint, and set up for Phase 3 constraint @@ -9810,27 +10026,27 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, * deletions. If it's a partitioned relation, every partition must be so * locked. * - * wqueue is the ALTER TABLE work queue; can be NULL when not running as part - * of an ALTER TABLE sequence. - * fkconstraint is the constraint being added. - * rel is the referencing relation; might be a partition, if recursing. - * pkrel is the root referenced relation. - * indexOid is the OID of the index (on pkrel) implementing this constraint. - * parentConstr is the OID of the parent constraint (there is always one). - * numfks is the number of columns in the foreign key - * pkattnum is the attnum array of referenced attributes. - * fkattnum is the attnum array of referencing attributes. - * pf/pp/ffeqoperators are OID array of operators between columns. - * numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT + * wqueue: the ALTER TABLE work queue; NULL when not running as part + * of an ALTER TABLE sequence. + * fkconstraint: the constraint being added + * rel: the referencing relation; might be a partition, if recursing + * pkrel: the root referenced relation + * indexOid: the OID of the index (on pkrel) implementing this constraint + * parentConstr: the OID of the parent constraint (there is always one) + * numfks: the number of columns in the foreign key + * pkattnum: the attnum array of referenced attributes + * fkattnum: the attnum array of referencing attributes + * pf/pp/ffeqoperators: OID array of operators between columns + * numfkdelsetcols: the number of columns in the ON DELETE SET NULL/DEFAULT * (...) clause - * fkdelsetcols is the attnum array of the columns in the ON DELETE SET + * fkdelsetcols: the attnum array of the columns in the ON DELETE SET * NULL/DEFAULT clause - * old_check_ok signals that this constraint replaces an existing one that - * was already validated (thus this one doesn't need validation). - * lockmode is the lockmode to acquire on partitions when recursing. - * parentInsTrigger and parentUpdTrigger, when being recursively called on - * a partition, are the OIDs of the parent check triggers for INSERT and - * UPDATE respectively. + * old_check_ok: true if this constraint replaces an existing one that + * was already validated (thus this one doesn't need validation) + * lockmode: the lockmode to acquire on partitions when recursing + * parentInsTrigger and parentUpdTrigger: when being recursively called on + * a partition, the OIDs of the parent check triggers for INSERT and + * UPDATE respectively. */ static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, @@ -9845,6 +10061,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, updateTriggerOid; Assert(OidIsValid(parentConstr)); + Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true)); + Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true)); if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) ereport(ERROR, @@ -9918,13 +10136,10 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, AttrMap *attmap; AttrNumber mapped_fkattnum[INDEX_MAX_KEYS]; bool attached; - char *conname; - Oid constrOid; - ObjectAddress address, - referenced; + ObjectAddress address; ListCell *cell; - CheckTableNotInUse(partition, "ALTER TABLE"); + CheckAlterTableIsSafe(partition); attmap = build_attrmap_by_name(RelationGetDescr(partition), RelationGetDescr(rel), @@ -9964,65 +10179,18 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, /* * No luck finding a good constraint to reuse; create our own. */ - if (ConstraintNameIsUsed(CONSTRAINT_RELATION, - RelationGetRelid(partition), - fkconstraint->conname)) - conname = ChooseConstraintName(RelationGetRelationName(partition), - ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs), - "fkey", - RelationGetNamespace(partition), NIL); - else - conname = fkconstraint->conname; - constrOid = - CreateConstraintEntry(conname, - RelationGetNamespace(partition), - CONSTRAINT_FOREIGN, - fkconstraint->deferrable, - fkconstraint->initdeferred, - fkconstraint->initially_valid, - parentConstr, - partitionId, - mapped_fkattnum, - numfks, - numfks, - InvalidOid, - indexOid, - RelationGetRelid(pkrel), - pkattnum, - pfeqoperators, - ppeqoperators, - ffeqoperators, - numfks, - fkconstraint->fk_upd_action, - fkconstraint->fk_del_action, - fkdelsetcols, - numfkdelsetcols, - fkconstraint->fk_matchtype, - NULL, - NULL, - NULL, - false, - 1, - false, - false); - - /* - * Give this constraint partition-type dependencies on the parent - * constraint as well as the table. - */ - ObjectAddressSet(address, ConstraintRelationId, constrOid); - ObjectAddressSet(referenced, ConstraintRelationId, parentConstr); - recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI); - ObjectAddressSet(referenced, RelationRelationId, partitionId); - recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC); - - /* Make all this visible before recursing */ - CommandCounterIncrement(); + address = addFkConstraint(addFkReferencingSide, + fkconstraint->conname, fkconstraint, + partition, pkrel, indexOid, parentConstr, + numfks, pkattnum, + mapped_fkattnum, pfeqoperators, + ppeqoperators, ffeqoperators, + numfkdelsetcols, fkdelsetcols, true); /* call ourselves to finalize the creation and we're done */ addFkRecurseReferencing(wqueue, fkconstraint, partition, pkrel, indexOid, - constrOid, + address.objectId, numfks, pkattnum, mapped_fkattnum, @@ -10064,14 +10232,14 @@ CloneForeignKeyConstraints(List **wqueue, Relation parentRel, Assert(parentRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); /* - * Clone constraints for which the parent is on the referenced side. + * First, clone constraints where the parent is on the referencing side. */ - CloneFkReferenced(parentRel, partitionRel); + CloneFkReferencing(wqueue, parentRel, partitionRel); /* - * Now clone constraints where the parent is on the referencing side. + * Clone constraints for which the parent is on the referenced side. */ - CloneFkReferencing(wqueue, parentRel, partitionRel); + CloneFkReferenced(parentRel, partitionRel); } /* @@ -10082,8 +10250,6 @@ CloneForeignKeyConstraints(List **wqueue, Relation parentRel, * clone those constraints to the given partition. This is to be called * when the partition is being created or attached. * - * This ignores self-referencing FKs; those are handled by CloneFkReferencing. - * * This recurses to partitions, if the relation being attached is partitioned. * Recursion is done by calling addFkRecurseReferenced. */ @@ -10155,6 +10321,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel) int numfkdelsetcols; AttrNumber confdelsetcols[INDEX_MAX_KEYS]; Constraint *fkconstraint; + ObjectAddress address; Oid deleteTriggerOid, updateTriggerOid; @@ -10173,24 +10340,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel) continue; } - /* - * Don't clone self-referencing foreign keys, which can be in the - * partitioned table or in the partition-to-be. - */ - if (constrForm->conrelid == RelationGetRelid(parentRel) || - constrForm->conrelid == RelationGetRelid(partitionRel)) - { - ReleaseSysCache(tuple); - continue; - } - - /* - * Because we're only expanding the key space at the referenced side, - * we don't need to prevent any operation in the referencing table, so - * AccessShareLock suffices (assumes that dropping the constraint - * acquires AEL). - */ - fkRel = table_open(constrForm->conrelid, AccessShareLock); + /* We need the same lock level that CreateTrigger will acquire */ + fkRel = table_open(constrForm->conrelid, ShareRowExclusiveLock); indexOid = constrForm->conindid; DeconstructFkConstraintRow(tuple, @@ -10254,12 +10405,19 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel) constrForm->confrelid, constrForm->conrelid, &deleteTriggerOid, &updateTriggerOid); - addFkRecurseReferenced(NULL, - fkconstraint, + /* Add this constraint ... */ + address = addFkConstraint(addFkReferencedSide, + fkconstraint->conname, fkconstraint, fkRel, + partitionRel, partIndexId, constrOid, + numfks, mapped_confkey, + conkey, conpfeqop, conppeqop, conffeqop, + numfkdelsetcols, confdelsetcols, false); + /* ... and recurse */ + addFkRecurseReferenced(fkconstraint, fkRel, partitionRel, partIndexId, - constrOid, + address.objectId, numfks, mapped_confkey, conkey, @@ -10289,8 +10447,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel) * child. * * If wqueue is given, it is used to set up phase-3 verification for each - * cloned constraint; if omitted, we assume that such verification is not - * needed (example: the partition is being created anew). + * cloned constraint; omit it if such verification is not needed + * (example: the partition is being created anew). */ static void CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel) @@ -10306,6 +10464,23 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel) { ForeignKeyCacheInfo *fk = lfirst(cell); + /* + * Refuse to attach a table as partition that this partitioned table + * already has a foreign key to. This isn't useful schema, which is + * proven by the fact that there have been no user complaints that + * it's already impossible to achieve this in the opposite direction, + * i.e., creating a foreign key that references a partition. This + * restriction allows us to dodge some complexities around + * pg_constraint and pg_trigger row creations that would be needed + * during ATTACH/DETACH for this kind of relationship. + */ + if (fk->confrelid == RelationGetRelid(partRel)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot attach table \"%s\" as a partition because it is referenced by foreign key \"%s\"", + RelationGetRelationName(partRel), + get_constraint_name(fk->conoid)))); + clone = lappend_oid(clone, fk->conoid); } @@ -10357,9 +10532,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel) Constraint *fkconstraint; bool attached; Oid indexOid; - Oid constrOid; - ObjectAddress address, - referenced; + ObjectAddress address; ListCell *lc; Oid insertTriggerOid, updateTriggerOid; @@ -10456,7 +10629,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel) fkconstraint->old_conpfeqop = NIL; fkconstraint->old_pktable_oid = InvalidOid; fkconstraint->skip_validation = false; - fkconstraint->initially_valid = true; + fkconstraint->initially_valid = constrForm->convalidated; for (int i = 0; i < numfks; i++) { Form_pg_attribute att; @@ -10466,71 +10639,29 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel) fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs, makeString(NameStr(att->attname))); } - if (ConstraintNameIsUsed(CONSTRAINT_RELATION, - RelationGetRelid(partRel), - NameStr(constrForm->conname))) - fkconstraint->conname = - ChooseConstraintName(RelationGetRelationName(partRel), - ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs), - "fkey", - RelationGetNamespace(partRel), NIL); - else - fkconstraint->conname = pstrdup(NameStr(constrForm->conname)); indexOid = constrForm->conindid; - constrOid = - CreateConstraintEntry(fkconstraint->conname, - constrForm->connamespace, - CONSTRAINT_FOREIGN, - fkconstraint->deferrable, - fkconstraint->initdeferred, - constrForm->convalidated, - parentConstrOid, - RelationGetRelid(partRel), - mapped_conkey, - numfks, - numfks, - InvalidOid, /* not a domain constraint */ - indexOid, - constrForm->confrelid, /* same foreign rel */ - confkey, - conpfeqop, - conppeqop, - conffeqop, - numfks, - fkconstraint->fk_upd_action, - fkconstraint->fk_del_action, - confdelsetcols, - numfkdelsetcols, - fkconstraint->fk_matchtype, - NULL, - NULL, - NULL, - false, /* islocal */ - 1, /* inhcount */ - false, /* conNoInherit */ - true); - - /* Set up partition dependencies for the new constraint */ - ObjectAddressSet(address, ConstraintRelationId, constrOid); - ObjectAddressSet(referenced, ConstraintRelationId, parentConstrOid); - recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI); - ObjectAddressSet(referenced, RelationRelationId, - RelationGetRelid(partRel)); - recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC); + + /* Create the pg_constraint entry at this level */ + address = addFkConstraint(addFkReferencingSide, + NameStr(constrForm->conname), fkconstraint, + partRel, pkrel, indexOid, parentConstrOid, + numfks, confkey, + mapped_conkey, conpfeqop, + conppeqop, conffeqop, + numfkdelsetcols, confdelsetcols, + false); /* Done with the cloned constraint's tuple */ ReleaseSysCache(tuple); - /* Make all this visible before recursing */ - CommandCounterIncrement(); - + /* Create the check triggers, and recurse to partitions, if any */ addFkRecurseReferencing(wqueue, fkconstraint, partRel, pkrel, indexOid, - constrOid, + address.objectId, numfks, confkey, mapped_conkey, @@ -10694,6 +10825,81 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk, TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger, partRelid); + /* + * If the referenced table is partitioned, then the partition we're + * attaching now has extra pg_constraint rows and action triggers that are + * no longer needed. Remove those. + */ + if (get_rel_relkind(fk->confrelid) == RELKIND_PARTITIONED_TABLE) + { + Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock); + ObjectAddresses *objs; + HeapTuple consttup; + + ScanKeyInit(&key, + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(fk->conrelid)); + + scan = systable_beginscan(pg_constraint, + ConstraintRelidTypidNameIndexId, + true, NULL, 1, &key); + objs = new_object_addresses(); + while ((consttup = systable_getnext(scan)) != NULL) + { + Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup); + + if (conform->conparentid != fk->conoid) + continue; + else + { + ObjectAddress addr; + SysScanDesc scan2; + ScanKeyData key2; + int n PG_USED_FOR_ASSERTS_ONLY; + + ObjectAddressSet(addr, ConstraintRelationId, conform->oid); + add_exact_object_address(&addr, objs); + + /* + * First we must delete the dependency record that binds the + * constraint records together. + */ + n = deleteDependencyRecordsForSpecific(ConstraintRelationId, + conform->oid, + DEPENDENCY_INTERNAL, + ConstraintRelationId, + fk->conoid); + Assert(n == 1); /* actually only one is expected */ + + /* + * Now search for the triggers for this constraint and set + * them up for deletion too + */ + ScanKeyInit(&key2, + Anum_pg_trigger_tgconstraint, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(conform->oid)); + scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId, + true, NULL, 1, &key2); + while ((trigtup = systable_getnext(scan2)) != NULL) + { + ObjectAddressSet(addr, TriggerRelationId, + ((Form_pg_trigger) GETSTRUCT(trigtup))->oid); + add_exact_object_address(&addr, objs); + } + systable_endscan(scan2); + } + } + /* make the dependency deletions visible */ + CommandCounterIncrement(); + performMultipleDeletions(objs, DROP_RESTRICT, + PERFORM_DELETION_INTERNAL); + systable_endscan(scan); + + table_close(pg_constraint, RowShareLock); + } + CommandCounterIncrement(); return true; } @@ -12045,7 +12251,7 @@ ATExecDropConstraint(Relation rel, const char *constrName, /* Must match lock taken by RemoveTriggerById: */ frel = table_open(con->confrelid, AccessExclusiveLock); - CheckTableNotInUse(frel, "ALTER TABLE"); + CheckAlterTableIsSafe(frel); table_close(frel, NoLock); } @@ -12123,7 +12329,7 @@ ATExecDropConstraint(Relation rel, const char *constrName, /* find_inheritance_children already got lock */ childrel = table_open(childrelid, NoLock); - CheckTableNotInUse(childrel, "ALTER TABLE"); + CheckAlterTableIsSafe(childrel); ScanKeyInit(&skey[0], Anum_pg_constraint_conrelid, @@ -12271,6 +12477,16 @@ ATPrepAlterColumnType(List **wqueue, colName))); /* + * Cannot specify USING when altering type of a generated column, because + * that would violate the generation expression. + */ + if (attTup->attgenerated && def->cooked_default) + ereport(ERROR, + (errcode(ERRCODE_INVALID_COLUMN_DEFINITION), + errmsg("cannot specify USING when altering type of generated column"), + errdetail("Column \"%s\" is a generated column.", colName))); + + /* * Don't alter inherited columns. At outer level, there had better not be * any inherited definition; when recursing, we assume this was checked at * the parent level (see below). @@ -12346,11 +12562,12 @@ ATPrepAlterColumnType(List **wqueue, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("column \"%s\" cannot be cast automatically to type %s", colName, format_type_be(targettype)), + !attTup->attgenerated ? /* translator: USING is SQL, don't translate it */ errhint("You might need to specify \"USING %s::%s\".", quote_identifier(colName), format_type_with_typemod(targettype, - targettypmod)))); + targettypmod)) : 0)); } /* Fix collations after all else */ @@ -12426,7 +12643,7 @@ ATPrepAlterColumnType(List **wqueue, /* find_all_inheritors already got lock */ childrel = relation_open(childrelid, NoLock); - CheckTableNotInUse(childrel, "ALTER TABLE"); + CheckAlterTableIsSafe(childrel); /* * Verify that the child doesn't have any inherited definitions of @@ -13272,9 +13489,12 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) /* * Re-parse the index and constraint definitions, and attach them to the * appropriate work queue entries. We do this before dropping because in - * the case of a FOREIGN KEY constraint, we might not yet have exclusive - * lock on the table the constraint is attached to, and we need to get - * that before reparsing/dropping. + * the case of a constraint on another table, we might not yet have + * exclusive lock on the table the constraint is attached to, and we need + * to get that before reparsing/dropping. (That's possible at least for + * FOREIGN KEY, CHECK, and EXCLUSION constraints; in non-FK cases it + * requires a dependency on the target table's composite type in the other + * table's constraint expressions.) * * We can't rely on the output of deparsing to tell us which relation to * operate on, because concurrent activity might have made the name @@ -13290,7 +13510,6 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) Form_pg_constraint con; Oid relid; Oid confrelid; - char contype; bool conislocal; tup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(oldId)); @@ -13307,7 +13526,6 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) elog(ERROR, "could not identify relation associated with constraint %u", oldId); } confrelid = con->confrelid; - contype = con->contype; conislocal = con->conislocal; ReleaseSysCache(tup); @@ -13324,12 +13542,12 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) continue; /* - * When rebuilding an FK constraint that references the table we're - * modifying, we might not yet have any lock on the FK's table, so get - * one now. We'll need AccessExclusiveLock for the DROP CONSTRAINT - * step, so there's no value in asking for anything weaker. + * When rebuilding another table's constraint that references the + * table we're modifying, we might not yet have any lock on the other + * table, so get one now. We'll need AccessExclusiveLock for the DROP + * CONSTRAINT step, so there's no value in asking for anything weaker. */ - if (relid != tab->relid && contype == CONSTRAINT_FOREIGN) + if (relid != tab->relid) LockRelationOid(relid, AccessExclusiveLock); ATPostAlterTypeParse(oldId, relid, confrelid, @@ -13343,6 +13561,14 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) Oid relid; relid = IndexGetRelation(oldId, false); + + /* + * As above, make sure we have lock on the index's table if it's not + * the same table. + */ + if (relid != tab->relid) + LockRelationOid(relid, AccessExclusiveLock); + ATPostAlterTypeParse(oldId, relid, InvalidOid, (char *) lfirst(def_item), wqueue, lockmode, tab->rewrite); @@ -13359,6 +13585,20 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) Oid relid; relid = StatisticsGetRelation(oldId, false); + + /* + * As above, make sure we have lock on the statistics object's table + * if it's not the same table. However, we take + * ShareUpdateExclusiveLock here, aligning with the lock level used in + * CreateStatistics and RemoveStatisticsById. + * + * CAUTION: this should be done after all cases that grab + * AccessExclusiveLock, else we risk causing deadlock due to needing + * to promote our table lock. + */ + if (relid != tab->relid) + LockRelationOid(relid, ShareUpdateExclusiveLock); + ATPostAlterTypeParse(oldId, relid, InvalidOid, (char *) lfirst(def_item), wqueue, lockmode, tab->rewrite); @@ -14358,7 +14598,7 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation, /* Fetch heap tuple */ relid = RelationGetRelid(rel); - tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid)); + tuple = SearchSysCacheLocked1(RELOID, ObjectIdGetDatum(relid)); if (!HeapTupleIsValid(tuple)) elog(ERROR, "cache lookup failed for relation %u", relid); @@ -14462,6 +14702,7 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation, repl_val, repl_null, repl_repl); CatalogTupleUpdate(pgclass, &newtuple->t_self, newtuple); + UnlockTuple(pgclass, &tuple->t_self, InplaceUpdateTupleLock); InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0); @@ -16618,7 +16859,8 @@ AlterRelationNamespaceInternal(Relation classRel, Oid relOid, ObjectAddress thisobj; bool already_done = false; - classTup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relOid)); + /* no rel lock for relkind=c so use LOCKTAG_TUPLE */ + classTup = SearchSysCacheLockedCopy1(RELOID, ObjectIdGetDatum(relOid)); if (!HeapTupleIsValid(classTup)) elog(ERROR, "cache lookup failed for relation %u", relOid); classForm = (Form_pg_class) GETSTRUCT(classTup); @@ -16637,6 +16879,8 @@ AlterRelationNamespaceInternal(Relation classRel, Oid relOid, already_done = object_address_present(&thisobj, objsMoved); if (!already_done && oldNspOid != newNspOid) { + ItemPointerData otid = classTup->t_self; + /* check for duplicate name (more friendly than unique-index failure) */ if (get_relname_relid(NameStr(classForm->relname), newNspOid) != InvalidOid) @@ -16649,7 +16893,9 @@ AlterRelationNamespaceInternal(Relation classRel, Oid relOid, /* classTup is a copy, so OK to scribble on */ classForm->relnamespace = newNspOid; - CatalogTupleUpdate(classRel, &classTup->t_self, classTup); + CatalogTupleUpdate(classRel, &otid, classTup); + UnlockTuple(classRel, &otid, InplaceUpdateTupleLock); + /* Update dependency on schema if caller said so */ if (hasDependEntry && @@ -16661,6 +16907,8 @@ AlterRelationNamespaceInternal(Relation classRel, Oid relOid, elog(ERROR, "failed to change schema dependency for relation \"%s\"", NameStr(classForm->relname)); } + else + UnlockTuple(classRel, &classTup->t_self, InplaceUpdateTupleLock); if (!already_done) { add_exact_object_address(&thisobj, objsMoved); @@ -18564,9 +18812,17 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel, tab->rel = rel; } + /* + * Detaching the partition might involve TOAST table access, so ensure we + * have a valid snapshot. + */ + PushActiveSnapshot(GetTransactionSnapshot()); + /* Do the final part of detaching */ DetachPartitionFinalize(rel, partRel, concurrent, defaultPartOid); + PopActiveSnapshot(); + ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel)); /* keep our lock until commit */ @@ -18595,6 +18851,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent, HeapTuple tuple, newtuple; Relation trigrel = NULL; + List *fkoids = NIL; if (concurrent) { @@ -18615,12 +18872,32 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent, fks = copyObject(RelationGetFKeyList(partRel)); if (fks != NIL) trigrel = table_open(TriggerRelationId, RowExclusiveLock); + + /* + * It's possible that the partition being detached has a foreign key that + * references a partitioned table. When that happens, there are multiple + * pg_constraint rows for the partition: one points to the partitioned + * table itself, while the others point to each of its partitions. Only + * the topmost one is to be considered here; the child constraints must be + * left alone, because conceptually those aren't coming from our parent + * partitioned table, but from this partition itself. + * + * We implement this by collecting all the constraint OIDs in a first scan + * of the FK array, and skipping in the loop below those constraints whose + * parents are listed here. + */ + foreach(cell, fks) + { + ForeignKeyCacheInfo *fk = (ForeignKeyCacheInfo *) lfirst(cell); + + fkoids = lappend_oid(fkoids, fk->conoid); + } + foreach(cell, fks) { ForeignKeyCacheInfo *fk = lfirst(cell); HeapTuple contup; Form_pg_constraint conform; - Constraint *fkconstraint; Oid insertTriggerOid, updateTriggerOid; @@ -18629,15 +18906,22 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent, elog(ERROR, "cache lookup failed for constraint %u", fk->conoid); conform = (Form_pg_constraint) GETSTRUCT(contup); - /* consider only the inherited foreign keys */ + /* + * Consider only inherited foreign keys, and only if their parents + * aren't in the list. + */ if (conform->contype != CONSTRAINT_FOREIGN || - !OidIsValid(conform->conparentid)) + !OidIsValid(conform->conparentid) || + list_member_oid(fkoids, conform->conparentid)) { ReleaseSysCache(contup); continue; } - /* unset conparentid and adjust conislocal, coninhcount, etc. */ + /* + * The constraint on this table must be marked no longer a child of + * the parent's constraint, as do its check triggers. + */ ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid); /* @@ -18655,33 +18939,87 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent, RelationGetRelid(partRel)); /* - * Make the action triggers on the referenced relation. When this was - * a partition the action triggers pointed to the parent rel (they - * still do), but now we need separate ones of our own. + * Lastly, create the action triggers on the referenced table, using + * addFkRecurseReferenced, which requires some elaborate setup (so put + * it in a separate block). While at it, if the table is partitioned, + * that function will recurse to create the pg_constraint rows and + * action triggers for each partition. + * + * Note there's no need to do addFkConstraint() here, because the + * pg_constraint row already exists. */ - fkconstraint = makeNode(Constraint); - fkconstraint->contype = CONSTRAINT_FOREIGN; - fkconstraint->conname = pstrdup(NameStr(conform->conname)); - fkconstraint->deferrable = conform->condeferrable; - fkconstraint->initdeferred = conform->condeferred; - fkconstraint->location = -1; - fkconstraint->pktable = NULL; - fkconstraint->fk_attrs = NIL; - fkconstraint->pk_attrs = NIL; - fkconstraint->fk_matchtype = conform->confmatchtype; - fkconstraint->fk_upd_action = conform->confupdtype; - fkconstraint->fk_del_action = conform->confdeltype; - fkconstraint->fk_del_set_cols = NIL; - fkconstraint->old_conpfeqop = NIL; - fkconstraint->old_pktable_oid = InvalidOid; - fkconstraint->skip_validation = false; - fkconstraint->initially_valid = true; + { + Constraint *fkconstraint; + int numfks; + AttrNumber conkey[INDEX_MAX_KEYS]; + AttrNumber confkey[INDEX_MAX_KEYS]; + Oid conpfeqop[INDEX_MAX_KEYS]; + Oid conppeqop[INDEX_MAX_KEYS]; + Oid conffeqop[INDEX_MAX_KEYS]; + int numfkdelsetcols; + AttrNumber confdelsetcols[INDEX_MAX_KEYS]; + Relation refdRel; + + DeconstructFkConstraintRow(contup, + &numfks, + conkey, + confkey, + conpfeqop, + conppeqop, + conffeqop, + &numfkdelsetcols, + confdelsetcols); + + /* Create a synthetic node we'll use throughout */ + fkconstraint = makeNode(Constraint); + fkconstraint->contype = CONSTRAINT_FOREIGN; + fkconstraint->conname = pstrdup(NameStr(conform->conname)); + fkconstraint->deferrable = conform->condeferrable; + fkconstraint->initdeferred = conform->condeferred; + fkconstraint->skip_validation = true; + fkconstraint->initially_valid = true; + /* a few irrelevant fields omitted here */ + fkconstraint->pktable = NULL; + fkconstraint->fk_attrs = NIL; + fkconstraint->pk_attrs = NIL; + fkconstraint->fk_matchtype = conform->confmatchtype; + fkconstraint->fk_upd_action = conform->confupdtype; + fkconstraint->fk_del_action = conform->confdeltype; + fkconstraint->fk_del_set_cols = NIL; + fkconstraint->old_conpfeqop = NIL; + fkconstraint->old_pktable_oid = InvalidOid; + fkconstraint->location = -1; + + /* set up colnames, used to generate the constraint name */ + for (int i = 0; i < numfks; i++) + { + Form_pg_attribute att; + + att = TupleDescAttr(RelationGetDescr(partRel), + conkey[i] - 1); + + fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs, + makeString(NameStr(att->attname))); + } + + refdRel = table_open(fk->confrelid, ShareRowExclusiveLock); - createForeignKeyActionTriggers(partRel, conform->confrelid, - fkconstraint, fk->conoid, - conform->conindid, - InvalidOid, InvalidOid, - NULL, NULL); + addFkRecurseReferenced(fkconstraint, partRel, + refdRel, + conform->conindid, + fk->conoid, + numfks, + confkey, + conkey, + conpfeqop, + conppeqop, + conffeqop, + numfkdelsetcols, + confdelsetcols, + true, + InvalidOid, InvalidOid); + table_close(refdRel, NoLock); /* keep lock till end of xact */ + } ReleaseSysCache(contup); } @@ -18715,22 +19053,31 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent, foreach(cell, indexes) { Oid idxid = lfirst_oid(cell); + Oid parentidx; Relation idx; Oid constrOid; + Oid parentConstrOid; if (!has_superclass(idxid)) continue; - Assert((IndexGetRelation(get_partition_parent(idxid, false), false) == - RelationGetRelid(rel))); + parentidx = get_partition_parent(idxid, false); + Assert((IndexGetRelation(parentidx, false) == RelationGetRelid(rel))); idx = index_open(idxid, AccessExclusiveLock); IndexSetParentIndex(idx, InvalidOid); - /* If there's a constraint associated with the index, detach it too */ + /* + * If there's a constraint associated with the index, detach it too. + * Careful: it is possible for a constraint index in a partition to be + * the child of a non-constraint index, so verify whether the parent + * index does actually have a constraint. + */ constrOid = get_relation_idx_constraint_oid(RelationGetRelid(partRel), idxid); - if (OidIsValid(constrOid)) + parentConstrOid = get_relation_idx_constraint_oid(RelationGetRelid(rel), + parentidx); + if (OidIsValid(parentConstrOid) && OidIsValid(constrOid)) ConstraintSetParentConstraint(constrOid, InvalidOid, InvalidOid); index_close(idx, NoLock); |
