=== Applying patches on top of PostgreSQL commit ID f4f77f557bb1a173f82be42770a28758732f4cc0 === /etc/rc.d/jail: WARNING: Per-jail configuration via jail_* variables is obsolete. Please consider migrating to /etc/jail.conf. Thu Jul 16 12:10:55 UTC 2026 On branch cf/6269 nothing to commit, working tree clean === using 'git am' to apply patch ./v3-0001-move-partitioning-code-out-of-tablecmds.c.patch === Applying: move partitioning code out of tablecmds.c Using index info to reconstruct a base tree... M src/backend/commands/Makefile M src/backend/commands/meson.build M src/backend/commands/tablecmds.c M src/backend/partitioning/partbounds.c M src/include/commands/tablecmds.h Falling back to patching base and 3-way merge... Auto-merging src/include/commands/tablecmds.h Auto-merging src/backend/partitioning/partbounds.c Auto-merging src/backend/commands/tablecmds.c CONFLICT (content): Merge conflict in src/backend/commands/tablecmds.c Auto-merging src/backend/commands/meson.build Auto-merging src/backend/commands/Makefile error: Failed to merge in the changes. hint: Use 'git am --show-current-patch=diff' to see the failed patch Patch failed at 0001 move partitioning code out of tablecmds.c When you have resolved this problem, run "git am --continue". If you prefer to skip this patch, run "git am --skip" instead. To restore the original branch and stop patching, run "git am --abort". === using patch(1) to apply patch ./v3-0001-move-partitioning-code-out-of-tablecmds.c.patch === patching file src/backend/commands/Makefile Hunk #1 succeeded at 59 (offset 2 lines). patching file src/backend/commands/meson.build Hunk #1 succeeded at 47 (offset 2 lines). patching file src/backend/commands/tablecmds.c Hunk #1 succeeded at 65 with fuzz 1 (offset 3 lines). Hunk #2 succeeded at 136 (offset 3 lines). Hunk #3 succeeded at 227 (offset 9 lines). Hunk #4 succeeded at 241 with fuzz 2 (offset 9 lines). Hunk #5 succeeded at 349 (offset 44 lines). Hunk #6 succeeded at 372 (offset 45 lines). Hunk #7 succeeded at 392 (offset 45 lines). Hunk #8 succeeded at 421 (offset 45 lines). Hunk #9 succeeded at 429 (offset 45 lines). Hunk #10 succeeded at 474 (offset 45 lines). Hunk #11 succeeded at 484 (offset 45 lines). Hunk #12 FAILED at 515. Hunk #13 succeeded at 4345 (offset 94 lines). Hunk #14 succeeded at 6487 (offset 96 lines). Hunk #15 succeeded at 6668 (offset 96 lines). Hunk #16 succeeded at 6799 (offset 112 lines). Hunk #17 succeeded at 8403 (offset 112 lines). Hunk #18 succeeded at 10644 (offset 118 lines). Hunk #19 succeeded at 10822 (offset 118 lines). Hunk #20 succeeded at 10960 (offset 118 lines). Hunk #21 succeeded at 11126 (offset 118 lines). Hunk #22 succeeded at 11136 (offset 118 lines). Hunk #23 succeeded at 11199 (offset 118 lines). Hunk #24 succeeded at 12395 (offset 504 lines). Hunk #25 succeeded at 16945 (offset 680 lines). Hunk #26 succeeded at 17426 (offset 675 lines). Hunk #27 succeeded at 17444 (offset 675 lines). Hunk #28 FAILED at 18541. Hunk #29 FAILED at 20899. 3 out of 29 hunks FAILED -- saving rejects to file src/backend/commands/tablecmds.c.rej patching file src/backend/commands/tablecmds_partition.c patching file src/backend/partitioning/partbounds.c patching file src/include/commands/tablecmds.h patching file src/include/commands/tablecmds_internal.h patching file src/include/commands/tablecmds_partition.h Unstaged changes after reset: M src/backend/commands/Makefile M src/backend/commands/meson.build M src/backend/commands/tablecmds.c M src/backend/partitioning/partbounds.c M src/include/commands/tablecmds.h Removing src/backend/commands/tablecmds.c.rej Removing src/backend/commands/tablecmds_partition.c Removing src/include/commands/tablecmds_internal.h Removing src/include/commands/tablecmds_partition.h === using 'git apply' to apply patch ./v3-0001-move-partitioning-code-out-of-tablecmds.c.patch === Applied patch to 'src/backend/commands/Makefile' cleanly. Applied patch to 'src/backend/commands/meson.build' cleanly. Applied patch to 'src/backend/commands/tablecmds.c' with conflicts. Falling back to direct application... Applied patch to 'src/backend/partitioning/partbounds.c' cleanly. Applied patch to 'src/include/commands/tablecmds.h' cleanly. Falling back to direct application... Falling back to direct application... U src/backend/commands/tablecmds.c diff --cc src/backend/commands/tablecmds.c index cb93c3e935a,4abba13a5b8..00000000000 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@@ -61,10 -59,11 +61,12 @@@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/extension.h" +#include "commands/repack.h" #include "commands/sequence.h" #include "commands/tablecmds.h" + #include "commands/tablecmds_internal.h" + #include "commands/tablecmds_partition.h" #include "commands/tablespace.h" #include "commands/trigger.h" #include "commands/typecmds.h" @@@ -359,35 -232,6 +241,27 @@@ typedef struct ForeignTruncateInf List *rels; } ForeignTruncateInfo; - /* Partial or complete FK creation in addFkConstraint() */ - typedef enum addFkConstraintSides - { - addFkReferencedSide, - addFkReferencingSide, - addFkBothSides, - } addFkConstraintSides; - +/* + * Hold extension dependencies of one partition index, during + * MERGE/SPLIT PARTITION processing. + * + * collectPartitionIndexExtDeps() builds a list of these entries sorted by + * parentIndexOid with exactly one entry per parent partitioned index; the + * list is then consumed by applyPartitionIndexExtDeps() to re-record the + * same dependencies on the newly created partition's indexes. + * + * extensionOids is kept sorted ascending so that equality checks between + * entries from different partitions can be done in a single pass. + * indexOid is carried only so that conflict errors can cite specific + * partition index names. + */ +typedef struct PartitionIndexExtDepEntry +{ + Oid parentIndexOid; /* OID of the parent partitioned index */ + Oid indexOid; /* OID of a representative partition index */ + List *extensionOids; /* OIDs of dependent extensions, sorted asc */ +} PartitionIndexExtDepEntry; + /* * Partition tables are expected to be dropped when the parent partitioned * table gets dropped. Hence for partitioning we use AUTO dependency. @@@ -756,45 -518,6 +564,18 @@@ static void RangeVarCallbackForAlterRel static char GetAttributeCompression(Oid atttypid, const char *compression); static char GetAttributeStorage(Oid atttypid, const char *storagemode); ++<<<<<<< ours +static void ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel, + PartitionCmd *cmd, AlterTableUtilityContext *context); +static void ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, + Relation rel, PartitionCmd *cmd, + AlterTableUtilityContext *context); +static List *collectPartitionIndexExtDeps(List *partitionOids); +static void applyPartitionIndexExtDeps(Oid newPartOid, List *extDepState); +static void freePartitionIndexExtDeps(List *extDepState); + ++======= ++>>>>>>> theirs /* ---------------------------------------------------------------- * DefineRelation * Creates a new relation. @@@ -12333,573 -11470,382 +11597,711 @@@ ATExecAlterConstrDeferrability(List **w } /* ++<<<<<<< ours + * ALTER TABLE ALTER CONSTRAINT + * + * Update the attributes of a constraint. + * + * Currently works for Foreign Key, Check, and not null constraints. + * + * If the constraint is modified, returns its address; otherwise, return + * InvalidObjectAddress. ++======= + * Returns true if the constraint's inheritability is altered. ++>>>>>>> theirs */ - static ObjectAddress - ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon, - bool recurse, LOCKMODE lockmode) + static bool + ATExecAlterConstrInheritability(List **wqueue, ATAlterConstraint *cmdcon, + Relation conrel, Relation rel, + HeapTuple contuple, LOCKMODE lockmode) { - Relation conrel; - Relation tgrel; - SysScanDesc scan; - ScanKeyData skey[3]; - HeapTuple contuple; Form_pg_constraint currcon; - ObjectAddress address; + AttrNumber colNum; + char *colName; + List *children; - /* - * Disallow altering ONLY a partitioned table, as it would make no sense. - * This is okay for legacy inheritance. - */ - if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE && !recurse) - ereport(ERROR, - errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("constraint must be altered in child tables too"), - errhint("Do not specify the ONLY keyword.")); + Assert(cmdcon->alterInheritability); + currcon = (Form_pg_constraint) GETSTRUCT(contuple); - conrel = table_open(ConstraintRelationId, RowExclusiveLock); - tgrel = table_open(TriggerRelationId, RowExclusiveLock); + /* The current implementation only works for NOT NULL constraints */ + Assert(currcon->contype == CONSTRAINT_NOTNULL); /* - * Find and check the target constraint + * If called to modify a constraint that's already in the desired state, + * silently do nothing. */ - ScanKeyInit(&skey[0], - Anum_pg_constraint_conrelid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(RelationGetRelid(rel))); - ScanKeyInit(&skey[1], - Anum_pg_constraint_contypid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(InvalidOid)); - ScanKeyInit(&skey[2], - Anum_pg_constraint_conname, - BTEqualStrategyNumber, F_NAMEEQ, - CStringGetDatum(cmdcon->conname)); - scan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId, - true, NULL, 3, skey); - - /* There can be at most one matching row */ - if (!HeapTupleIsValid(contuple = systable_getnext(scan))) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("constraint \"%s\" of relation \"%s\" does not exist", - cmdcon->conname, RelationGetRelationName(rel)))); + if (cmdcon->noinherit == currcon->connoinherit) + return false; ++<<<<<<< ours + currcon = (Form_pg_constraint) GETSTRUCT(contuple); + if (cmdcon->alterDeferrability && currcon->contype != CONSTRAINT_FOREIGN) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint", + cmdcon->conname, RelationGetRelationName(rel)))); + if (cmdcon->alterEnforceability && + (currcon->contype != CONSTRAINT_FOREIGN && currcon->contype != CONSTRAINT_CHECK)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot alter enforceability of constraint \"%s\" of relation \"%s\"", + cmdcon->conname, RelationGetRelationName(rel)), + errhint("Only foreign key and check constraints can change enforceability."))); + if (cmdcon->alterInheritability && + currcon->contype != CONSTRAINT_NOTNULL) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("constraint \"%s\" of relation \"%s\" is not a not-null constraint", + cmdcon->conname, RelationGetRelationName(rel))); + if (cmdcon->alterInheritability && + cmdcon->noinherit && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("not-null constraint \"%s\" on partitioned table \"%s\" cannot be NO INHERIT", + cmdcon->conname, RelationGetRelationName(rel))); ++======= + AlterConstrUpdateConstraintEntry(cmdcon, conrel, contuple); + CommandCounterIncrement(); ++>>>>>>> theirs - /* Refuse to modify inheritability of inherited constraints */ - if (cmdcon->alterInheritability && - cmdcon->noinherit && currcon->coninhcount > 0) - ereport(ERROR, - errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("cannot alter inherited constraint \"%s\" on relation \"%s\"", - NameStr(currcon->conname), - RelationGetRelationName(rel))); + /* Fetch the column number and name */ + colNum = extractNotNullColumn(contuple); + colName = get_attname(currcon->conrelid, colNum, false); /* - * If it's not the topmost constraint, raise an error. - * - * Altering a non-topmost constraint leaves some triggers untouched, since - * they are not directly connected to this constraint; also, pg_dump would - * ignore the deferrability status of the individual constraint, since it - * only dumps topmost constraints. Avoid these problems by refusing this - * operation and telling the user to alter the parent constraint instead. + * Propagate the change to children. For this subcommand type we don't + * recursively affect children, just the immediate level. */ - if (OidIsValid(currcon->conparentid)) + children = find_inheritance_children(RelationGetRelid(rel), + lockmode); + foreach_oid(childoid, children) { - HeapTuple tp; - Oid parent = currcon->conparentid; - char *ancestorname = NULL; - char *ancestortable = NULL; + ObjectAddress addr; - /* Loop to find the topmost constraint */ - while (HeapTupleIsValid(tp = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parent)))) + if (cmdcon->noinherit) { - Form_pg_constraint contup = (Form_pg_constraint) GETSTRUCT(tp); - - /* If no parent, this is the constraint we want */ - if (!OidIsValid(contup->conparentid)) - { - ancestorname = pstrdup(NameStr(contup->conname)); - ancestortable = get_rel_name(contup->conrelid); - ReleaseSysCache(tp); - break; - } + HeapTuple childtup; + Form_pg_constraint childcon; - parent = contup->conparentid; - ReleaseSysCache(tp); + childtup = findNotNullConstraint(childoid, colName); + if (!childtup) + elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u", + colName, childoid); + childcon = (Form_pg_constraint) GETSTRUCT(childtup); + Assert(childcon->coninhcount > 0); + childcon->coninhcount--; + childcon->conislocal = true; + CatalogTupleUpdate(conrel, &childtup->t_self, childtup); + heap_freetuple(childtup); } + else + { + Relation childrel = table_open(childoid, NoLock); - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("cannot alter constraint \"%s\" on relation \"%s\"", - cmdcon->conname, RelationGetRelationName(rel)), - ancestorname && ancestortable ? - errdetail("Constraint \"%s\" is derived from constraint \"%s\" of relation \"%s\".", - cmdcon->conname, ancestorname, ancestortable) : 0, - errhint("You may alter the constraint it derives from instead."))); + addr = ATExecSetNotNull(wqueue, childrel, NameStr(currcon->conname), + colName, true, true, lockmode); + if (OidIsValid(addr.objectId)) + CommandCounterIncrement(); + table_close(childrel, NoLock); + } } - address = InvalidObjectAddress; - - /* - * Do the actual catalog work, and recurse if necessary. - */ - if (ATExecAlterConstraintInternal(wqueue, cmdcon, conrel, tgrel, rel, - contuple, recurse, lockmode)) - ObjectAddressSet(address, ConstraintRelationId, currcon->oid); - - systable_endscan(scan); - - table_close(tgrel, RowExclusiveLock); - table_close(conrel, RowExclusiveLock); - - return address; + return true; } /* - * A subroutine of ATExecAlterConstraint that calls the respective routines for - * altering constraint's enforceability, deferrability or inheritability. + * A subroutine of ATExecAlterConstrDeferrability that updated constraint + * trigger's deferrability. + * + * The arguments to this function have the same meaning as the arguments to + * ATExecAlterConstrDeferrability. */ - static bool - ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon, - Relation conrel, Relation tgrel, Relation rel, - HeapTuple contuple, bool recurse, - LOCKMODE lockmode) + static void + AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel, + bool deferrable, bool initdeferred, + List **otherrelids) { - Form_pg_constraint currcon; - bool changed = false; - List *otherrelids = NIL; + HeapTuple tgtuple; + ScanKeyData tgkey; + SysScanDesc tgscan; - currcon = (Form_pg_constraint) GETSTRUCT(contuple); + ScanKeyInit(&tgkey, + Anum_pg_trigger_tgconstraint, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(conoid)); + tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true, + NULL, 1, &tgkey); + while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan))) + { + Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple); + Form_pg_trigger copy_tg; + HeapTuple tgCopyTuple; ++<<<<<<< ours + /* + * Do the catalog work for the enforceability or deferrability change, + * recurse if necessary. + * + * Note that even if deferrability is requested to be altered along with + * enforceability, we don't need to explicitly update multiple entries in + * pg_trigger related to deferrability. + * + * Modifying foreign key enforceability involves either creating or + * dropping the trigger, during which the deferrability setting will be + * adjusted automatically. + */ + if (cmdcon->alterEnforceability) + { + if (currcon->contype == CONSTRAINT_FOREIGN) + changed = ATExecAlterFKConstrEnforceability(wqueue, cmdcon, conrel, tgrel, + currcon->conrelid, + currcon->confrelid, + contuple, lockmode, + InvalidOid, InvalidOid, + InvalidOid, InvalidOid); + else if (currcon->contype == CONSTRAINT_CHECK) + changed = ATExecAlterCheckConstrEnforceability(wqueue, cmdcon, conrel, + contuple, recurse, false, + NIL, lockmode); + } + else if (cmdcon->alterDeferrability && + ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, rel, + contuple, recurse, &otherrelids, + lockmode)) + { ++======= /* - * AlterConstrUpdateConstraintEntry already invalidated relcache for - * the relations having the constraint itself; here we also invalidate - * for relations that have any triggers that are part of the - * constraint. + * Remember OIDs of other relation(s) involved in FK constraint. + * (Note: it's likely that we could skip forcing a relcache inval for + * other rels that don't have a trigger whose properties change, but + * let's be conservative.) */ - foreach_oid(relid, otherrelids) - CacheInvalidateRelcacheByRelid(relid); + if (tgform->tgrelid != RelationGetRelid(rel)) + *otherrelids = list_append_unique_oid(*otherrelids, + tgform->tgrelid); - changed = true; - } ++>>>>>>> theirs + /* + * Update enable status and deferrability of RI_FKey_noaction_del, + * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd + * triggers, but not others; see createForeignKeyActionTriggers and + * CreateFKCheckTrigger. + */ + if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL && + tgform->tgfoid != F_RI_FKEY_NOACTION_UPD && + tgform->tgfoid != F_RI_FKEY_CHECK_INS && + tgform->tgfoid != F_RI_FKEY_CHECK_UPD) + continue; - /* - * Do the catalog work for the inheritability change. - */ - if (cmdcon->alterInheritability && - ATExecAlterConstrInheritability(wqueue, cmdcon, conrel, rel, contuple, - lockmode)) - changed = true; + tgCopyTuple = heap_copytuple(tgtuple); + copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple); - return changed; + copy_tg->tgdeferrable = deferrable; + copy_tg->tginitdeferred = initdeferred; + CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple); + + InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0); + + heap_freetuple(tgCopyTuple); + } + + systable_endscan(tgscan); } /* ++<<<<<<< ours + * Returns true if the foreign key constraint's enforceability is altered. ++======= + * Invokes ATExecAlterConstrEnforceability for each constraint that is a child of + * the specified constraint. ++>>>>>>> theirs * - * Depending on whether the constraint is being set to ENFORCED or NOT - * ENFORCED, it creates or drops the trigger accordingly. + * Note that this doesn't handle recursion the normal way, viz. by scanning the + * list of child relations and recursing; instead it uses the conparentid + * relationships. This may need to be reconsidered. * - * Note that we must recurse even when trying to change a constraint to not - * enforced if it is already not enforced, in case descendant constraints - * might be enforced and need to be changed to not enforced. Conversely, we - * should do nothing if a constraint is being set to enforced and is already - * enforced, as descendant constraints cannot be different in that case. + * The arguments to this function have the same meaning as the arguments to + * ATExecAlterConstrEnforceability. */ ++<<<<<<< ours +static bool +ATExecAlterFKConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, + Relation conrel, Relation tgrel, + Oid fkrelid, Oid pkrelid, + HeapTuple contuple, LOCKMODE lockmode, + Oid ReferencedParentDelTrigger, + Oid ReferencedParentUpdTrigger, + Oid ReferencingParentInsTrigger, + Oid ReferencingParentUpdTrigger) +{ + Form_pg_constraint currcon; + Oid conoid; + Relation rel; + bool changed = false; + + /* Since this function recurses, it could be driven to stack overflow */ + check_stack_depth(); + + Assert(cmdcon->alterEnforceability); ++======= + static void + AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, + Relation conrel, Relation tgrel, + Oid fkrelid, Oid pkrelid, + HeapTuple contuple, LOCKMODE lockmode, + Oid ReferencedParentDelTrigger, + Oid ReferencedParentUpdTrigger, + Oid ReferencingParentInsTrigger, + Oid ReferencingParentUpdTrigger) + { + Form_pg_constraint currcon; + Oid conoid; + ScanKeyData pkey; + SysScanDesc pscan; + HeapTuple childtup; ++>>>>>>> theirs currcon = (Form_pg_constraint) GETSTRUCT(contuple); conoid = currcon->oid; - /* Should be foreign key constraint */ - Assert(currcon->contype == CONSTRAINT_FOREIGN); - - rel = table_open(currcon->conrelid, lockmode); - - if (currcon->conenforced != cmdcon->is_enforced) - { - AlterConstrUpdateConstraintEntry(cmdcon, conrel, contuple); - changed = true; - } + ScanKeyInit(&pkey, + Anum_pg_constraint_conparentid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(conoid)); ++<<<<<<< ours + /* Drop triggers */ + if (!cmdcon->is_enforced) + { + /* + * When setting a constraint to NOT ENFORCED, the constraint triggers + * need to be dropped. Therefore, we must process the child relations + * first, followed by the parent, to account for dependencies. + */ + if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE || + get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE) + AlterFKConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, tgrel, + fkrelid, pkrelid, contuple, + lockmode, InvalidOid, InvalidOid, + InvalidOid, InvalidOid); ++======= + pscan = systable_beginscan(conrel, ConstraintParentIndexId, + true, NULL, 1, &pkey); ++>>>>>>> theirs - /* Drop all the triggers */ - DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid); - } - else if (changed) /* Create triggers */ - { - Oid ReferencedDelTriggerOid = InvalidOid, - ReferencedUpdTriggerOid = InvalidOid, - ReferencingInsTriggerOid = InvalidOid, - ReferencingUpdTriggerOid = InvalidOid; + while (HeapTupleIsValid(childtup = systable_getnext(pscan))) + ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid, + pkrelid, childtup, lockmode, + ReferencedParentDelTrigger, + ReferencedParentUpdTrigger, + ReferencingParentInsTrigger, + ReferencingParentUpdTrigger); - /* Prepare the minimal information required for trigger creation. */ - Constraint *fkconstraint = makeNode(Constraint); + systable_endscan(pscan); + } ++<<<<<<< ours + fkconstraint->conname = pstrdup(NameStr(currcon->conname)); + fkconstraint->fk_matchtype = currcon->confmatchtype; + fkconstraint->fk_upd_action = currcon->confupdtype; + fkconstraint->fk_del_action = currcon->confdeltype; + fkconstraint->deferrable = currcon->condeferrable; + fkconstraint->initdeferred = currcon->condeferred; ++======= + /* + * Invokes ATExecAlterConstrDeferrability for each constraint that is a child of + * the specified constraint. + * + * Note that this doesn't handle recursion the normal way, viz. by scanning the + * list of child relations and recursing; instead it uses the conparentid + * relationships. This may need to be reconsidered. + * + * The arguments to this function have the same meaning as the arguments to + * ATExecAlterConstrDeferrability. + */ + static void + AlterConstrDeferrabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, + Relation conrel, Relation tgrel, Relation rel, + HeapTuple contuple, bool recurse, + List **otherrelids, LOCKMODE lockmode) + { + Form_pg_constraint currcon; + Oid conoid; + ScanKeyData pkey; + SysScanDesc pscan; + HeapTuple childtup; ++>>>>>>> theirs - /* Create referenced triggers */ - if (currcon->conrelid == fkrelid) - createForeignKeyActionTriggers(currcon->conrelid, - currcon->confrelid, - fkconstraint, - conoid, - currcon->conindid, - ReferencedParentDelTrigger, - ReferencedParentUpdTrigger, - &ReferencedDelTriggerOid, - &ReferencedUpdTriggerOid); + currcon = (Form_pg_constraint) GETSTRUCT(contuple); + conoid = currcon->oid; - /* Create referencing triggers */ - if (currcon->confrelid == pkrelid) - createForeignKeyCheckTriggers(currcon->conrelid, - pkrelid, - fkconstraint, - conoid, - currcon->conindid, - ReferencingParentInsTrigger, - ReferencingParentUpdTrigger, - &ReferencingInsTriggerOid, - &ReferencingUpdTriggerOid); + ScanKeyInit(&pkey, + Anum_pg_constraint_conparentid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(conoid)); - /* - * Tell Phase 3 to check that the constraint is satisfied by existing - * rows. Only applies to leaf partitions, and (for constraints that - * reference a partitioned table) only if this is not one of the - * pg_constraint rows that exist solely to support action triggers. - */ - if (rel->rd_rel->relkind == RELKIND_RELATION && - currcon->confrelid == pkrelid) - { - AlteredTableInfo *tab; - NewConstraint *newcon; + pscan = systable_beginscan(conrel, ConstraintParentIndexId, + true, NULL, 1, &pkey); - newcon = palloc0_object(NewConstraint); - newcon->name = fkconstraint->conname; - newcon->contype = CONSTR_FOREIGN; - newcon->refrelid = currcon->confrelid; - newcon->refindid = currcon->conindid; - newcon->conid = currcon->oid; - newcon->qual = (Node *) fkconstraint; + while (HeapTupleIsValid(childtup = systable_getnext(pscan))) + { + Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup); + Relation childrel; - /* Find or create work queue entry for this table */ - tab = ATGetQueueEntry(wqueue, rel); - tab->constraints = lappend(tab->constraints, newcon); - } + childrel = table_open(childcon->conrelid, lockmode); ++<<<<<<< ours + /* + * If the table at either end of the constraint is partitioned, we + * need to recurse and create triggers for each constraint that is a + * child of this one. + */ + if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE || + get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE) + AlterFKConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, tgrel, + fkrelid, pkrelid, contuple, + lockmode, + ReferencedDelTriggerOid, + ReferencedUpdTriggerOid, + ReferencingInsTriggerOid, + ReferencingUpdTriggerOid); ++======= + ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, childrel, + childtup, recurse, otherrelids, lockmode); + table_close(childrel, NoLock); ++>>>>>>> theirs } - table_close(rel, NoLock); - - return changed; + systable_endscan(pscan); } /* ++<<<<<<< ours + * Returns true if the CHECK constraint's enforceability is altered. + */ +static bool +ATExecAlterCheckConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, + Relation conrel, HeapTuple contuple, + bool recurse, bool recursing, + List *changing_conids, + LOCKMODE lockmode) +{ + Form_pg_constraint currcon; + Relation rel; + bool changed = false; + List *children = NIL; + bool target_enforced = cmdcon->is_enforced; + Oid enforced_parentoid = InvalidOid; + + /* Since this function recurses, it could be driven to stack overflow */ + check_stack_depth(); + + Assert(cmdcon->alterEnforceability); + + currcon = (Form_pg_constraint) GETSTRUCT(contuple); + + Assert(currcon->contype == CONSTRAINT_CHECK); + + /* + * Parent relation already locked by caller, children will be locked by + * find_all_inheritors. So NoLock is fine here. + */ + rel = table_open(currcon->conrelid, NoLock); + + /* + * When setting a constraint to NOT ENFORCED, check whether any matching + * parent constraint remains ENFORCED and is not part of this ALTER. + * + * For a direct ALTER of an inherited constraint, reject the command, + * because the child cannot be weakened while its parent remains enforced. + * + * During recursion, another parent outside this ALTER may still enforce + * the same constraint in a regular inheritance hierarchy. In that case, + * keep the child constraint ENFORCED so that its merged enforceability + * still reflects the remaining enforced parent. Partitions do not need + * this recursive parent check because a partition can have only one + * direct parent. + */ + if (!cmdcon->is_enforced && + (!recursing || !rel->rd_rel->relispartition) && + ATCheckCheckConstrHasEnforcedParent(conrel, rel, contuple, + changing_conids, + &enforced_parentoid)) + { + if (!recursing) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot mark inherited constraint \"%s\" as %s", + NameStr(currcon->conname), + "NOT ENFORCED"), + errdetail("The matching constraint on parent table \"%s\" is %s.", + get_rel_name(enforced_parentoid), "ENFORCED")); + + target_enforced = true; ++======= + * Update the constraint entry for the given ATAlterConstraint command, and + * invoke the appropriate hooks. + */ + static void + AlterConstrUpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation conrel, + HeapTuple contuple) + { + HeapTuple copyTuple; + Form_pg_constraint copy_con; + + Assert(cmdcon->alterEnforceability || cmdcon->alterDeferrability || + cmdcon->alterInheritability); + + copyTuple = heap_copytuple(contuple); + copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); + + if (cmdcon->alterEnforceability) + { + copy_con->conenforced = cmdcon->is_enforced; + + /* + * NB: The convalidated status is irrelevant when the constraint is + * set to NOT ENFORCED, but for consistency, it should still be set + * appropriately. Similarly, if the constraint is later changed to + * ENFORCED, validation will be performed during phase 3, so it makes + * sense to mark it as valid in that case. + */ + copy_con->convalidated = cmdcon->is_enforced; ++>>>>>>> theirs + } + if (cmdcon->alterDeferrability) + { + copy_con->condeferrable = cmdcon->deferrable; + copy_con->condeferred = cmdcon->initdeferred; } + if (cmdcon->alterInheritability) + copy_con->connoinherit = cmdcon->noinherit; ++<<<<<<< ours + /* + * Update to the merged enforceability if needed. This may differ from the + * requested enforceability when another matching parent constraint + * remains enforced. + */ + if (currcon->conenforced != target_enforced) + { + ATAlterConstraint updatecon = *cmdcon; + + updatecon.is_enforced = target_enforced; + AlterConstrUpdateConstraintEntry(&updatecon, conrel, contuple); + changed = true; + } + + /* + * Note that we must recurse even when trying to change a check constraint + * to not enforced if it is already not enforced, in case descendant + * constraints might be enforced and need to be changed to not enforced, + * unless they still inherit an enforced constraint from another parent. + * Conversely, we should do nothing if a constraint is being set to + * enforced and is already enforced, as descendant constraints cannot be + * different in that case. + */ + if (!cmdcon->is_enforced || changed) + { + /* + * If we're recursing, the parent has already done this, so skip it. + * Also, if the constraint is a NO INHERIT constraint, we shouldn't + * try to look for it in the children. + */ + if (!recursing && !currcon->connoinherit) + { + Assert(changing_conids == NIL); + + children = find_all_inheritors(RelationGetRelid(rel), + lockmode, NULL); + + /* + * When setting NOT ENFORCED, build the set of equivalent CHECK + * constraints that this command will attempt to change before + * visiting descendants. The root itself has already been checked + * above. + */ + if (!cmdcon->is_enforced) + changing_conids = list_make1_oid(currcon->oid); + + foreach_oid(childoid, children) + { + if (childoid == RelationGetRelid(rel)) + continue; + + /* + * If we are told not to recurse, there had better not be any + * child tables, because we can't change constraint + * enforceability on the parent unless we have changed + * enforceability for all child. + */ + if (!recurse) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("constraint must be altered on child tables too"), + errhint("Do not specify the ONLY keyword.")); + + /* + * It is sufficient to look up the constraint by name here. + * Supported DDL ensures that inheritable CHECK constraints + * with the same name have equivalent definitions when they + * are propagated to children or when inheritance is + * established. All descendants returned by + * find_all_inheritors must have this constraint: inherited + * CHECK constraints propagate to all children at + * inheritance-link creation time and cannot be dropped + * independently on child tables. + */ + if (!cmdcon->is_enforced) + changing_conids = + list_append_unique_oid(changing_conids, + get_relation_constraint_oid(childoid, + cmdcon->conname, + false)); + } + } + + foreach_oid(childoid, children) + { + if (childoid == RelationGetRelid(rel)) + continue; + + AlterCheckConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, + childoid, false, true, + changing_conids, + lockmode); + } + } + + /* + * Tell Phase 3 to check that the constraint is satisfied by existing + * rows. We only need do this when altering the constraint from NOT + * ENFORCED to ENFORCED. + */ + if (rel->rd_rel->relkind == RELKIND_RELATION && + !currcon->conenforced && + target_enforced) + { + AlteredTableInfo *tab; + NewConstraint *newcon; + Datum val; + char *conbin; + + newcon = palloc0_object(NewConstraint); + newcon->name = pstrdup(NameStr(currcon->conname)); + newcon->contype = CONSTR_CHECK; + + val = SysCacheGetAttrNotNull(CONSTROID, contuple, + Anum_pg_constraint_conbin); + conbin = TextDatumGetCString(val); + newcon->qual = expand_generated_columns_in_expr(stringToNode(conbin), rel, 1); + + /* Find or create work queue entry for this table */ + tab = ATGetQueueEntry(wqueue, rel); + tab->constraints = lappend(tab->constraints, newcon); ++======= + CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple); + InvokeObjectPostAlterHook(ConstraintRelationId, copy_con->oid, 0); + + /* Make new constraint flags visible to others */ + CacheInvalidateRelcacheByRelid(copy_con->conrelid); + + heap_freetuple(copyTuple); + } + + /* + * ALTER TABLE VALIDATE CONSTRAINT + * + * XXX The reason we handle recursion here rather than at Phase 1 is because + * there's no good way to skip recursing when handling foreign keys: there is + * no need to lock children in that case, yet we wouldn't be able to avoid + * doing so at that level. + * + * Return value is the address of the validated constraint. If the constraint + * was already validated, InvalidObjectAddress is returned. + */ + static ObjectAddress + ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName, + bool recurse, bool recursing, LOCKMODE lockmode) + { + Relation conrel; + SysScanDesc scan; + ScanKeyData skey[3]; + HeapTuple tuple; + Form_pg_constraint con; + ObjectAddress address; + + conrel = table_open(ConstraintRelationId, RowExclusiveLock); + + /* + * Find and check the target constraint + */ + ScanKeyInit(&skey[0], + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(rel))); + ScanKeyInit(&skey[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + ScanKeyInit(&skey[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(constrName)); + scan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId, + true, NULL, 3, skey); + + /* There can be at most one matching row */ + if (!HeapTupleIsValid(tuple = systable_getnext(scan))) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("constraint \"%s\" of relation \"%s\" does not exist", + constrName, RelationGetRelationName(rel)))); + + con = (Form_pg_constraint) GETSTRUCT(tuple); + if (con->contype != CONSTRAINT_FOREIGN && + con->contype != CONSTRAINT_CHECK && + con->contype != CONSTRAINT_NOTNULL) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot validate constraint \"%s\" of relation \"%s\"", + constrName, RelationGetRelationName(rel)), + errdetail("This operation is not supported for this type of constraint.")); + + if (!con->conenforced) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot validate NOT ENFORCED constraint"))); + + if (!con->convalidated) + { + if (con->contype == CONSTRAINT_FOREIGN) + { + QueueFKConstraintValidation(wqueue, conrel, rel, con->confrelid, + tuple, lockmode); + } + else if (con->contype == CONSTRAINT_CHECK) + { + QueueCheckConstraintValidation(wqueue, conrel, rel, constrName, + tuple, recurse, recursing, lockmode); + } + else if (con->contype == CONSTRAINT_NOTNULL) + { + QueueNNConstraintValidation(wqueue, conrel, rel, + tuple, recurse, recursing, lockmode); + } + + ObjectAddressSet(address, ConstraintRelationId, con->oid); ++>>>>>>> theirs } + else + address = InvalidObjectAddress; /* already validated */ - systable_endscan(scan); - - table_close(conrel, RowExclusiveLock); ++<<<<<<< ours + table_close(rel, NoLock); - return address; + return changed; } /* @@@ -13218,688 -12174,536 +12620,475 @@@ ATExecAlterConstrInheritability(List ** } /* - * transformColumnNameList - transform list of column names - * - * Lookup each name and return its attnum and, optionally, type and collation - * OIDs + * A subroutine of ATExecAlterConstrDeferrability that updated constraint + * trigger's deferrability. * - * Note: the name of this function suggests that it's general-purpose, - * but actually it's only used to look up names appearing in foreign-key - * clauses. The error messages would need work to use it in other cases, - * and perhaps the validity checks as well. + * The arguments to this function have the same meaning as the arguments to + * ATExecAlterConstrDeferrability. */ -static int -transformColumnNameList(Oid relId, List *colList, - int16 *attnums, Oid *atttypids, Oid *attcollids) +static void +AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel, + bool deferrable, bool initdeferred, + List **otherrelids) { - ListCell *l; - int attnum; + HeapTuple tgtuple; + ScanKeyData tgkey; + SysScanDesc tgscan; - attnum = 0; - foreach(l, colList) + ScanKeyInit(&tgkey, + Anum_pg_trigger_tgconstraint, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(conoid)); + tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true, + NULL, 1, &tgkey); + while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan))) { - char *attname = strVal(lfirst(l)); - HeapTuple atttuple; - Form_pg_attribute attform; + Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple); + Form_pg_trigger copy_tg; + HeapTuple tgCopyTuple; - atttuple = SearchSysCacheAttName(relId, attname); - if (!HeapTupleIsValid(atttuple)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("column \"%s\" referenced in foreign key constraint does not exist", - attname))); - attform = (Form_pg_attribute) GETSTRUCT(atttuple); - if (attform->attnum < 0) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("system columns cannot be used in foreign keys"))); - if (attnum >= INDEX_MAX_KEYS) - ereport(ERROR, - (errcode(ERRCODE_TOO_MANY_COLUMNS), - errmsg("cannot have more than %d keys in a foreign key", - INDEX_MAX_KEYS))); - attnums[attnum] = attform->attnum; - if (atttypids != NULL) - atttypids[attnum] = attform->atttypid; - if (attcollids != NULL) - attcollids[attnum] = attform->attcollation; - ReleaseSysCache(atttuple); - attnum++; - } + /* + * Remember OIDs of other relation(s) involved in FK constraint. + * (Note: it's likely that we could skip forcing a relcache inval for + * other rels that don't have a trigger whose properties change, but + * let's be conservative.) + */ + if (tgform->tgrelid != RelationGetRelid(rel)) + *otherrelids = list_append_unique_oid(*otherrelids, + tgform->tgrelid); ++======= ++ systable_endscan(scan); - /* - * Update enable status and deferrability of RI_FKey_noaction_del, - * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd - * triggers, but not others; see createForeignKeyActionTriggers and - * CreateFKCheckTrigger. - */ - if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL && - tgform->tgfoid != F_RI_FKEY_NOACTION_UPD && - tgform->tgfoid != F_RI_FKEY_CHECK_INS && - tgform->tgfoid != F_RI_FKEY_CHECK_UPD) - continue; - - tgCopyTuple = heap_copytuple(tgtuple); - copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple); - - copy_tg->tgdeferrable = deferrable; - copy_tg->tginitdeferred = initdeferred; - CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple); - - InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0); - - heap_freetuple(tgCopyTuple); - } - return attnum; ++ table_close(conrel, RowExclusiveLock); + - systable_endscan(tgscan); ++ return address; } /* - * Invokes ATExecAlterFKConstrEnforceability for each foreign key constraint - * that is a child of the specified constraint. - * transformFkeyGetPrimaryKey - -- * - * Note that this doesn't handle recursion the normal way, viz. by scanning the - * list of child relations and recursing; instead it uses the conparentid - * relationships. This may need to be reconsidered. - * Look up the names, attnums, types, and collations of the primary key attributes - * for the pkrel. Also return the index OID and index opclasses of the - * index supporting the primary key. Also return whether the index has - * WITHOUT OVERLAPS. - * - * All parameters except pkrel are output parameters. Also, the function - * return value is the number of attributes in the primary key. ++ * QueueFKConstraintValidation * - * The arguments to this function have the same meaning as the arguments to - * ATExecAlterFKConstrEnforceability. - * Used when the column list in the REFERENCES specification is omitted. ++ * Add an entry to the wqueue to validate the given foreign key constraint in ++ * Phase 3 and update the convalidated field in the pg_constraint catalog ++ * for the specified relation and all its children. */ - static void - AlterFKConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, - Relation conrel, Relation tgrel, - Oid fkrelid, Oid pkrelid, - HeapTuple contuple, LOCKMODE lockmode, - Oid ReferencedParentDelTrigger, - Oid ReferencedParentUpdTrigger, - Oid ReferencingParentInsTrigger, - Oid ReferencingParentUpdTrigger) -static int -transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid, - List **attnamelist, - int16 *attnums, Oid *atttypids, Oid *attcollids, - Oid *opclasses, bool *pk_has_without_overlaps) ++void ++QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation fkrel, ++ Oid pkrelid, HeapTuple contuple, LOCKMODE lockmode) { - Form_pg_constraint currcon; - Oid conoid; - ScanKeyData pkey; - SysScanDesc pscan; - HeapTuple childtup; - - currcon = (Form_pg_constraint) GETSTRUCT(contuple); - conoid = currcon->oid; - List *indexoidlist; - ListCell *indexoidscan; - HeapTuple indexTuple = NULL; - Form_pg_index indexStruct = NULL; - Datum indclassDatum; - oidvector *indclass; - int i; ++ Form_pg_constraint con; ++ AlteredTableInfo *tab; ++ HeapTuple copyTuple; ++ Form_pg_constraint copy_con; + - ScanKeyInit(&pkey, - Anum_pg_constraint_conparentid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(conoid)); ++ con = (Form_pg_constraint) GETSTRUCT(contuple); ++ Assert(con->contype == CONSTRAINT_FOREIGN); ++ Assert(!con->convalidated); - pscan = systable_beginscan(conrel, ConstraintParentIndexId, - true, NULL, 1, &pkey); + /* - * Get the list of index OIDs for the table from the relcache, and look up - * each one in the pg_index syscache until we find one marked primary key - * (hopefully there isn't more than one such). Insist it's valid, too. ++ * Add the validation to phase 3's queue; not needed for partitioned ++ * tables themselves, only for their partitions. ++ * ++ * When the referenced table (pkrelid) is partitioned, the referencing ++ * table (fkrel) has one pg_constraint row pointing to each partition ++ * thereof. These rows are there only to support action triggers and no ++ * table scan is needed, therefore skip this for them as well. + */ - *indexOid = InvalidOid; ++ if (fkrel->rd_rel->relkind == RELKIND_RELATION && ++ con->confrelid == pkrelid) ++ { ++ NewConstraint *newcon; ++ Constraint *fkconstraint; - while (HeapTupleIsValid(childtup = systable_getnext(pscan))) - ATExecAlterFKConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid, - pkrelid, childtup, lockmode, - ReferencedParentDelTrigger, - ReferencedParentUpdTrigger, - ReferencingParentInsTrigger, - ReferencingParentUpdTrigger); - indexoidlist = RelationGetIndexList(pkrel); ++ /* Queue validation for phase 3 */ ++ fkconstraint = makeNode(Constraint); ++ /* for now this is all we need */ ++ fkconstraint->conname = pstrdup(NameStr(con->conname)); ++>>>>>>> theirs - systable_endscan(pscan); - } - foreach(indexoidscan, indexoidlist) ++ newcon = palloc0_object(NewConstraint); ++ newcon->name = fkconstraint->conname; ++ newcon->contype = CONSTR_FOREIGN; ++ newcon->refrelid = con->confrelid; ++ newcon->refindid = con->conindid; ++ newcon->conid = con->oid; ++ newcon->qual = (Node *) fkconstraint; ++ ++ /* Find or create work queue entry for this table */ ++ tab = ATGetQueueEntry(wqueue, fkrel); ++ tab->constraints = lappend(tab->constraints, newcon); ++ } ++ ++ /* ++ * If the table at either end of the constraint is partitioned, we need to ++ * recurse and handle every unvalidated constraint that is a child of this ++ * constraint. ++ */ ++ if (fkrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE || ++ get_rel_relkind(con->confrelid) == RELKIND_PARTITIONED_TABLE) + { - Oid indexoid = lfirst_oid(indexoidscan); ++ ScanKeyData pkey; ++ SysScanDesc pscan; ++ HeapTuple childtup; + - indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid)); - if (!HeapTupleIsValid(indexTuple)) - elog(ERROR, "cache lookup failed for index %u", indexoid); - indexStruct = (Form_pg_index) GETSTRUCT(indexTuple); - if (indexStruct->indisprimary && indexStruct->indisvalid) ++ ScanKeyInit(&pkey, ++ Anum_pg_constraint_conparentid, ++ BTEqualStrategyNumber, F_OIDEQ, ++ ObjectIdGetDatum(con->oid)); ++ ++ pscan = systable_beginscan(conrel, ConstraintParentIndexId, ++ true, NULL, 1, &pkey); ++ ++ while (HeapTupleIsValid(childtup = systable_getnext(pscan))) + { ++ Form_pg_constraint childcon; ++ Relation childrel; + ++<<<<<<< ours +/* - * Invokes ATExecAlterConstrDeferrability for each constraint that is a child of - * the specified constraint. ++ * Invokes ATExecAlterFKConstrEnforceability for each foreign key constraint ++ * that is a child of the specified constraint. + * + * Note that this doesn't handle recursion the normal way, viz. by scanning the + * list of child relations and recursing; instead it uses the conparentid + * relationships. This may need to be reconsidered. + * + * The arguments to this function have the same meaning as the arguments to - * ATExecAlterConstrDeferrability. ++ * ATExecAlterFKConstrEnforceability. + */ +static void - AlterConstrDeferrabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, - Relation conrel, Relation tgrel, Relation rel, - HeapTuple contuple, bool recurse, - List **otherrelids, LOCKMODE lockmode) ++AlterFKConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, ++ Relation conrel, Relation tgrel, ++ Oid fkrelid, Oid pkrelid, ++ HeapTuple contuple, LOCKMODE lockmode, ++ Oid ReferencedParentDelTrigger, ++ Oid ReferencedParentUpdTrigger, ++ Oid ReferencingParentInsTrigger, ++ Oid ReferencingParentUpdTrigger) +{ + Form_pg_constraint currcon; + Oid conoid; + ScanKeyData pkey; + SysScanDesc pscan; + HeapTuple childtup; ++======= ++ childcon = (Form_pg_constraint) GETSTRUCT(childtup); ++>>>>>>> theirs + - currcon = (Form_pg_constraint) GETSTRUCT(contuple); - conoid = currcon->oid; + /* - * Refuse to use a deferrable primary key. This is per SQL spec, - * and there would be a lot of interesting semantic problems if we - * tried to allow it. ++ * If the child constraint has already been validated, no further ++ * action is required for it or its descendants, as they are all ++ * valid. + */ - if (!indexStruct->indimmediate) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("cannot use a deferrable primary key for referenced table \"%s\"", - RelationGetRelationName(pkrel)))); - - *indexOid = indexoid; - break; - } - ReleaseSysCache(indexTuple); - } ++ if (childcon->convalidated) ++ continue; - ScanKeyInit(&pkey, - Anum_pg_constraint_conparentid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(conoid)); - list_free(indexoidlist); ++ childrel = table_open(childcon->conrelid, lockmode); - pscan = systable_beginscan(conrel, ConstraintParentIndexId, - true, NULL, 1, &pkey); - /* - * Check that we found it - */ - if (!OidIsValid(*indexOid)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("there is no primary key for referenced table \"%s\"", - RelationGetRelationName(pkrel)))); ++ /* ++ * NB: Note that pkrelid should be passed as-is during recursion, ++ * as it is required to identify the root referenced table. ++ */ ++ QueueFKConstraintValidation(wqueue, conrel, childrel, pkrelid, ++ childtup, lockmode); ++ table_close(childrel, NoLock); ++ } - /* Must get indclass the hard way */ - indclassDatum = SysCacheGetAttrNotNull(INDEXRELID, indexTuple, - Anum_pg_index_indclass); - indclass = (oidvector *) DatumGetPointer(indclassDatum); ++<<<<<<< ours + while (HeapTupleIsValid(childtup = systable_getnext(pscan))) - { - Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup); - Relation childrel; ++ ATExecAlterFKConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid, ++ pkrelid, childtup, lockmode, ++ ReferencedParentDelTrigger, ++ ReferencedParentUpdTrigger, ++ ReferencingParentInsTrigger, ++ ReferencingParentUpdTrigger); ++======= ++ systable_endscan(pscan); ++ } ++>>>>>>> theirs - childrel = table_open(childcon->conrelid, lockmode); + /* - * Now build the list of PK attributes from the indkey definition (we - * assume a primary key cannot have expressional elements) ++ * Now mark the pg_constraint row as validated (even if we didn't check, ++ * notably the ones for partitions on the referenced side). ++ * ++ * We rely on transaction abort to roll back this change if phase 3 ++ * ultimately finds violating rows. This is a bit ugly. + */ - *attnamelist = NIL; - for (i = 0; i < indexStruct->indnkeyatts; i++) - { - int pkattno = indexStruct->indkey.values[i]; - - attnums[i] = pkattno; - atttypids[i] = attnumTypeId(pkrel, pkattno); - attcollids[i] = attnumCollationId(pkrel, pkattno); - opclasses[i] = indclass->values[i]; - *attnamelist = lappend(*attnamelist, - makeString(pstrdup(NameStr(*attnumAttName(pkrel, pkattno))))); - } - - *pk_has_without_overlaps = indexStruct->indisexclusion; ++ copyTuple = heap_copytuple(contuple); ++ copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); ++ copy_con->convalidated = true; ++ CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple); - ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, childrel, - childtup, recurse, otherrelids, lockmode); - table_close(childrel, NoLock); - } - ReleaseSysCache(indexTuple); ++ InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0); - systable_endscan(pscan); - return i; ++ heap_freetuple(copyTuple); } /* - * Update the constraint entry for the given ATAlterConstraint command, and - * invoke the appropriate hooks. - * transformFkeyCheckAttrs - - * - * Validate that the 'attnums' columns in the 'pkrel' relation are valid to - * reference as part of a foreign key constraint. - * - * Returns the OID of the unique index supporting the constraint and - * populates the caller-provided 'opclasses' array with the opclasses - * associated with the index columns. Also sets whether the index - * uses WITHOUT OVERLAPS. ++ * QueueCheckConstraintValidation + * - * Raises an ERROR on validation failure. ++ * Add an entry to the wqueue to validate the given check constraint in Phase 3 ++ * and update the convalidated field in the pg_constraint catalog for the ++ * specified relation and all its inheriting children. */ -static Oid -transformFkeyCheckAttrs(Relation pkrel, - int numattrs, int16 *attnums, - bool with_period, Oid *opclasses, - bool *pk_has_without_overlaps) +static void - AlterConstrUpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation conrel, - HeapTuple contuple) ++QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel, ++ char *constrName, HeapTuple contuple, ++ bool recurse, bool recursing, LOCKMODE lockmode) { - Oid indexoid = InvalidOid; - bool found = false; - bool found_deferrable = false; - List *indexoidlist; - ListCell *indexoidscan; - int i, - j; ++ Form_pg_constraint con; ++ AlteredTableInfo *tab; + HeapTuple copyTuple; + Form_pg_constraint copy_con; + - Assert(cmdcon->alterEnforceability || cmdcon->alterDeferrability || - cmdcon->alterInheritability); ++ List *children = NIL; ++ ListCell *child; ++ NewConstraint *newcon; ++ Datum val; ++ char *conbin; + - copyTuple = heap_copytuple(contuple); - copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); ++ con = (Form_pg_constraint) GETSTRUCT(contuple); ++ Assert(con->contype == CONSTRAINT_CHECK); - if (cmdcon->alterEnforceability) + /* - * Reject duplicate appearances of columns in the referenced-columns list. - * Such a case is forbidden by the SQL standard, and even if we thought it - * useful to allow it, there would be ambiguity about how to match the - * list to unique indexes (in particular, it'd be unclear which index - * opclass goes with which FK column). ++ * If we're recursing, the parent has already done this, so skip it. Also, ++ * if the constraint is a NO INHERIT constraint, we shouldn't try to look ++ * for it in the children. + */ - for (i = 0; i < numattrs; i++) - { - for (j = i + 1; j < numattrs; j++) - { - if (attnums[i] == attnums[j]) - ereport(ERROR, - (errcode(ERRCODE_INVALID_FOREIGN_KEY), - errmsg("foreign key referenced-columns list must not contain duplicates"))); - } - } ++ if (!recursing && !con->connoinherit) ++ children = find_all_inheritors(RelationGetRelid(rel), ++ lockmode, NULL); + + /* - * Get the list of index OIDs for the table from the relcache, and look up - * each one in the pg_index syscache, and match unique indexes to the list - * of attnums we are given. ++ * For CHECK constraints, we must ensure that we only mark the constraint ++ * as validated on the parent if it's already validated on the children. ++ * ++ * We recurse before validating on the parent, to reduce risk of ++ * deadlocks. + */ - indexoidlist = RelationGetIndexList(pkrel); - - foreach(indexoidscan, indexoidlist) ++ foreach(child, children) { - copy_con->conenforced = cmdcon->is_enforced; - HeapTuple indexTuple; - Form_pg_index indexStruct; ++ Oid childoid = lfirst_oid(child); ++ Relation childrel; + - indexoid = lfirst_oid(indexoidscan); - indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid)); - if (!HeapTupleIsValid(indexTuple)) - elog(ERROR, "cache lookup failed for index %u", indexoid); - indexStruct = (Form_pg_index) GETSTRUCT(indexTuple); ++ if (childoid == RelationGetRelid(rel)) ++ continue; /* - * NB: The convalidated status is irrelevant when the constraint is - * set to NOT ENFORCED, but for consistency, it should still be set - * appropriately. Similarly, if the constraint is later changed to - * ENFORCED, validation will be performed during phase 3, so it makes - * sense to mark it as valid in that case. - * Must have the right number of columns; must be unique (or if - * temporal then exclusion instead) and not a partial index; forget it - * if there are any expressions, too. Invalid indexes are out as well. ++ * If we are told not to recurse, there had better not be any child ++ * tables, because we can't mark the constraint on the parent valid ++ * unless it is valid for all child tables. */ - copy_con->convalidated = cmdcon->is_enforced; - } - if (cmdcon->alterDeferrability) - { - copy_con->condeferrable = cmdcon->deferrable; - copy_con->condeferred = cmdcon->initdeferred; - if (indexStruct->indnkeyatts == numattrs && - (with_period ? indexStruct->indisexclusion : indexStruct->indisunique) && - indexStruct->indisvalid && - heap_attisnull(indexTuple, Anum_pg_index_indpred, NULL) && - heap_attisnull(indexTuple, Anum_pg_index_indexprs, NULL)) - { - Datum indclassDatum; - oidvector *indclass; ++ if (!recurse) ++ ereport(ERROR, ++ (errcode(ERRCODE_INVALID_TABLE_DEFINITION), ++ errmsg("constraint must be validated on child tables too"))); + - /* Must get indclass the hard way */ - indclassDatum = SysCacheGetAttrNotNull(INDEXRELID, indexTuple, - Anum_pg_index_indclass); - indclass = (oidvector *) DatumGetPointer(indclassDatum); ++ /* find_all_inheritors already got lock */ ++ childrel = table_open(childoid, NoLock); + - /* - * The given attnum list may match the index columns in any order. - * Check for a match, and extract the appropriate opclasses while - * we're at it. - * - * We know that attnums[] is duplicate-free per the test at the - * start of this function, and we checked above that the number of - * index columns agrees, so if we find a match for each attnums[] - * entry then we must have a one-to-one match in some order. - */ - for (i = 0; i < numattrs; i++) - { - found = false; - for (j = 0; j < numattrs; j++) - { - if (attnums[i] == indexStruct->indkey.values[j]) - { - opclasses[i] = indclass->values[j]; - found = true; - break; - } - } - if (!found) - break; - } - /* The last attribute in the index must be the PERIOD FK part */ - if (found && with_period) - { - int16 periodattnum = attnums[numattrs - 1]; - - found = (periodattnum == indexStruct->indkey.values[numattrs - 1]); - } - - /* - * Refuse to use a deferrable unique/primary key. This is per SQL - * spec, and there would be a lot of interesting semantic problems - * if we tried to allow it. - */ - if (found && !indexStruct->indimmediate) - { - /* - * Remember that we found an otherwise matching index, so that - * we can generate a more appropriate error message. - */ - found_deferrable = true; - found = false; - } - - /* We need to know whether the index has WITHOUT OVERLAPS */ - if (found) - *pk_has_without_overlaps = indexStruct->indisexclusion; - } - ReleaseSysCache(indexTuple); - if (found) - break; - } - - if (!found) - { - if (found_deferrable) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("cannot use a deferrable unique constraint for referenced table \"%s\"", - RelationGetRelationName(pkrel)))); - else - ereport(ERROR, - (errcode(ERRCODE_INVALID_FOREIGN_KEY), - errmsg("there is no unique constraint matching given keys for referenced table \"%s\"", - RelationGetRelationName(pkrel)))); ++ ATExecValidateConstraint(wqueue, childrel, constrName, false, ++ true, lockmode); ++ table_close(childrel, NoLock); } - if (cmdcon->alterInheritability) - copy_con->connoinherit = cmdcon->noinherit; - list_free(indexoidlist); ++ /* Queue validation for phase 3 */ ++ newcon = palloc0_object(NewConstraint); ++ newcon->name = constrName; ++ newcon->contype = CONSTR_CHECK; ++ newcon->refrelid = InvalidOid; ++ newcon->refindid = InvalidOid; ++ newcon->conid = con->oid; + - return indexoid; -} ++ val = SysCacheGetAttrNotNull(CONSTROID, contuple, ++ Anum_pg_constraint_conbin); ++ conbin = TextDatumGetCString(val); ++ newcon->qual = expand_generated_columns_in_expr(stringToNode(conbin), rel, 1); + -/* - * 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; ++ /* Find or create work queue entry for this table */ ++ tab = ATGetQueueEntry(wqueue, rel); ++ tab->constraints = lappend(tab->constraints, newcon); + - 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); - } ++ /* ++ * Invalidate relcache so that others see the new validated constraint. ++ */ ++ CacheInvalidateRelcache(rel); + - return ret; -} ++ /* ++ * Now update the catalog, while we have the door open. ++ */ ++ copyTuple = heap_copytuple(contuple); ++ copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); ++ copy_con->convalidated = true; + CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple); - InvokeObjectPostAlterHook(ConstraintRelationId, copy_con->oid, 0); - /* Make new constraint flags visible to others */ - CacheInvalidateRelcacheByRelid(copy_con->conrelid); -/* - * Permissions checks on the referenced table for ADD FOREIGN KEY - * - * Note: we have already checked that the user owns the referencing table, - * else we'd have failed much earlier; no additional checks are needed for it. - */ -static void -checkFkeyPermissions(Relation rel, int16 *attnums, int natts) -{ - Oid roleid = GetUserId(); - AclResult aclresult; - int i; ++ InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0); - /* Okay if we have relation-level REFERENCES permission */ - aclresult = pg_class_aclcheck(RelationGetRelid(rel), roleid, - ACL_REFERENCES); - if (aclresult == ACLCHECK_OK) - return; - /* Else we must have REFERENCES on each column */ - for (i = 0; i < natts; i++) - { - aclresult = pg_attribute_aclcheck(RelationGetRelid(rel), attnums[i], - roleid, ACL_REFERENCES); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind), - RelationGetRelationName(rel)); - } + heap_freetuple(copyTuple); } /* - * ALTER TABLE VALIDATE CONSTRAINT - * - * XXX The reason we handle recursion here rather than at Phase 1 is because - * there's no good way to skip recursing when handling foreign keys: there is - * no need to lock children in that case, yet we wouldn't be able to avoid - * doing so at that level. - * Scan the existing rows in a table to verify they meet a proposed FK - * constraint. ++ * QueueNNConstraintValidation * - * Return value is the address of the validated constraint. If the constraint - * was already validated, InvalidObjectAddress is returned. - * Caller must have opened and locked both relations appropriately. ++ * Add an entry to the wqueue to validate the given not-null constraint in ++ * Phase 3 and update the convalidated field in the pg_constraint catalog for ++ * the specified relation and all its inheriting children. */ - static ObjectAddress - ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName, - bool recurse, bool recursing, LOCKMODE lockmode) + static void -validateForeignKeyConstraint(char *conname, - Relation rel, - Relation pkrel, - Oid pkindOid, - Oid constraintOid, - bool hasperiod) ++QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel, ++ HeapTuple contuple, bool recurse, bool recursing, ++ LOCKMODE lockmode) { - Relation conrel; - SysScanDesc scan; - ScanKeyData skey[3]; - HeapTuple tuple; - TupleTableSlot *slot; - TableScanDesc scan; - Trigger trig = {0}; - Snapshot snapshot; - MemoryContext oldcxt; - MemoryContext perTupCxt; - - ereport(DEBUG1, - (errmsg_internal("validating foreign key constraint \"%s\"", conname))); + Form_pg_constraint con; - ObjectAddress address; - - conrel = table_open(ConstraintRelationId, RowExclusiveLock); - - /* - * Find and check the target constraint - */ - ScanKeyInit(&skey[0], - Anum_pg_constraint_conrelid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(RelationGetRelid(rel))); - ScanKeyInit(&skey[1], - Anum_pg_constraint_contypid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(InvalidOid)); - ScanKeyInit(&skey[2], - Anum_pg_constraint_conname, - BTEqualStrategyNumber, F_NAMEEQ, - CStringGetDatum(constrName)); - scan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId, - true, NULL, 3, skey); ++ AlteredTableInfo *tab; ++ HeapTuple copyTuple; ++ Form_pg_constraint copy_con; ++ List *children = NIL; ++ AttrNumber attnum; ++ char *colname; - /* There can be at most one matching row */ - if (!HeapTupleIsValid(tuple = systable_getnext(scan))) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("constraint \"%s\" of relation \"%s\" does not exist", - constrName, RelationGetRelationName(rel)))); - /* - * Build a trigger call structure; we'll need it either way. - */ - trig.tgoid = InvalidOid; - trig.tgname = conname; - trig.tgenabled = TRIGGER_FIRES_ON_ORIGIN; - trig.tgisinternal = true; - trig.tgconstrrelid = RelationGetRelid(pkrel); - trig.tgconstrindid = pkindOid; - trig.tgconstraint = constraintOid; - trig.tgdeferrable = false; - trig.tginitdeferred = false; - /* we needn't fill in remaining fields */ ++ con = (Form_pg_constraint) GETSTRUCT(contuple); ++ Assert(con->contype == CONSTRAINT_NOTNULL); - con = (Form_pg_constraint) GETSTRUCT(tuple); - if (con->contype != CONSTRAINT_FOREIGN && - con->contype != CONSTRAINT_CHECK && - con->contype != CONSTRAINT_NOTNULL) - ereport(ERROR, - errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot validate constraint \"%s\" of relation \"%s\"", - constrName, RelationGetRelationName(rel)), - errdetail("This operation is not supported for this type of constraint.")); - /* - * See if we can do it with a single LEFT JOIN query. A false result - * indicates we must proceed with the fire-the-trigger method. We can't do - * a LEFT JOIN for temporal FKs yet, but we can once we support temporal - * left joins. - */ - if (!hasperiod && RI_Initial_Check(&trig, rel, pkrel)) - return; ++ attnum = extractNotNullColumn(contuple); - if (!con->conenforced) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("cannot validate NOT ENFORCED constraint"))); + /* - * Scan through each tuple, calling RI_FKey_check_ins (insert trigger) as - * if that tuple had just been inserted. If any of those fail, it should - * ereport(ERROR) and that's that. ++ * If we're recursing, we've already done this for parent, so skip it. ++ * Also, if the constraint is a NO INHERIT constraint, we shouldn't try to ++ * look for it in the children. ++ * ++ * We recurse before validating on the parent, to reduce risk of ++ * deadlocks. + */ - snapshot = RegisterSnapshot(GetLatestSnapshot()); - slot = table_slot_create(rel, NULL); - scan = table_beginscan(rel, snapshot, 0, NULL); - - perTupCxt = AllocSetContextCreate(CurrentMemoryContext, - "validateForeignKeyConstraint", - ALLOCSET_SMALL_SIZES); - oldcxt = MemoryContextSwitchTo(perTupCxt); ++ if (!recursing && !con->connoinherit) ++ children = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL); - if (!con->convalidated) - while (table_scan_getnextslot(scan, ForwardScanDirection, slot)) ++ colname = get_attname(RelationGetRelid(rel), attnum, false); ++ foreach_oid(childoid, children) { - if (con->contype == CONSTRAINT_FOREIGN) - { - QueueFKConstraintValidation(wqueue, conrel, rel, con->confrelid, - tuple, lockmode); - } - else if (con->contype == CONSTRAINT_CHECK) - { - QueueCheckConstraintValidation(wqueue, conrel, rel, constrName, - tuple, recurse, recursing, lockmode); - } - else if (con->contype == CONSTRAINT_NOTNULL) - { - QueueNNConstraintValidation(wqueue, conrel, rel, - tuple, recurse, recursing, lockmode); - } - - ObjectAddressSet(address, ConstraintRelationId, con->oid); - } - else - address = InvalidObjectAddress; /* already validated */ - - systable_endscan(scan); - - table_close(conrel, RowExclusiveLock); - LOCAL_FCINFO(fcinfo, 0); - TriggerData trigdata = {0}; ++ Relation childrel; ++ HeapTuple contup; ++ Form_pg_constraint childcon; ++ char *conname; - return address; - } - CHECK_FOR_INTERRUPTS(); ++ if (childoid == RelationGetRelid(rel)) ++ continue; - /* - * QueueFKConstraintValidation - * - * Add an entry to the wqueue to validate the given foreign key constraint in - * Phase 3 and update the convalidated field in the pg_constraint catalog - * for the specified relation and all its children. - */ - static void - QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation fkrel, - Oid pkrelid, HeapTuple contuple, LOCKMODE lockmode) - { - Form_pg_constraint con; - AlteredTableInfo *tab; - HeapTuple copyTuple; - Form_pg_constraint copy_con; + /* - * Make a call to the trigger function - * - * No parameters are passed, but we do set a context ++ * If we are told not to recurse, there had better not be any child ++ * tables, because we can't mark the constraint on the parent valid ++ * unless it is valid for all child tables. + */ - MemSet(fcinfo, 0, SizeForFunctionCallInfo(0)); ++ if (!recurse) ++ ereport(ERROR, ++ errcode(ERRCODE_INVALID_TABLE_DEFINITION), ++ errmsg("constraint must be validated on child tables too")); ++<<<<<<< ours + /* since this function recurses, it could be driven to stack overflow */ + check_stack_depth(); + + con = (Form_pg_constraint) GETSTRUCT(contuple); + Assert(con->contype == CONSTRAINT_FOREIGN); + Assert(!con->convalidated); ++======= + /* - * We assume RI_FKey_check_ins won't look at flinfo... - */ - trigdata.type = T_TriggerData; - trigdata.tg_event = TRIGGER_EVENT_INSERT | TRIGGER_EVENT_ROW; - trigdata.tg_relation = rel; - trigdata.tg_trigtuple = ExecFetchSlotHeapTuple(slot, false, NULL); - trigdata.tg_trigslot = slot; - trigdata.tg_trigger = &trig; - - fcinfo->context = (Node *) &trigdata; ++ * The column on child might have a different attnum, so search by ++ * column name. ++ */ ++ contup = findNotNullConstraint(childoid, colname); ++ if (!contup) ++ elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"", ++ colname, get_rel_name(childoid)); ++ childcon = (Form_pg_constraint) GETSTRUCT(contup); ++ if (childcon->convalidated) ++ continue; ++>>>>>>> theirs - /* - * Add the validation to phase 3's queue; not needed for partitioned - * tables themselves, only for their partitions. - * - * When the referenced table (pkrelid) is partitioned, the referencing - * table (fkrel) has one pg_constraint row pointing to each partition - * thereof. These rows are there only to support action triggers and no - * table scan is needed, therefore skip this for them as well. - */ - if (fkrel->rd_rel->relkind == RELKIND_RELATION && - con->confrelid == pkrelid) - { - NewConstraint *newcon; - Constraint *fkconstraint; - RI_FKey_check_ins(fcinfo); ++ /* find_all_inheritors already got lock */ ++ childrel = table_open(childoid, NoLock); ++ conname = pstrdup(NameStr(childcon->conname)); - /* Queue validation for phase 3 */ - fkconstraint = makeNode(Constraint); - /* for now this is all we need */ - fkconstraint->conname = pstrdup(NameStr(con->conname)); - MemoryContextReset(perTupCxt); ++ /* XXX improve ATExecValidateConstraint API to avoid double search */ ++ ATExecValidateConstraint(wqueue, childrel, conname, ++ false, true, lockmode); ++ table_close(childrel, NoLock); + } - newcon = palloc0_object(NewConstraint); - newcon->name = fkconstraint->conname; - newcon->contype = CONSTR_FOREIGN; - newcon->refrelid = con->confrelid; - newcon->refindid = con->conindid; - newcon->conid = con->oid; - newcon->qual = (Node *) fkconstraint; - MemoryContextSwitchTo(oldcxt); - MemoryContextDelete(perTupCxt); - table_endscan(scan); - UnregisterSnapshot(snapshot); - ExecDropSingleTupleTableSlot(slot); ++ /* Set attnotnull appropriately without queueing another validation */ ++ set_attnotnull(NULL, rel, attnum, true, false); + - /* Find or create work queue entry for this table */ - tab = ATGetQueueEntry(wqueue, fkrel); - tab->constraints = lappend(tab->constraints, newcon); - } ++ tab = ATGetQueueEntry(wqueue, rel); ++ tab->verify_new_notnull = true; + + /* - * If the table at either end of the constraint is partitioned, we need to - * recurse and handle every unvalidated constraint that is a child of this - * constraint. ++ * Invalidate relcache so that others see the new validated constraint. + */ - if (fkrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE || - get_rel_relkind(con->confrelid) == RELKIND_PARTITIONED_TABLE) - { - ScanKeyData pkey; - SysScanDesc pscan; - HeapTuple childtup; - - ScanKeyInit(&pkey, - Anum_pg_constraint_conparentid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(con->oid)); - - pscan = systable_beginscan(conrel, ConstraintParentIndexId, - true, NULL, 1, &pkey); - - while (HeapTupleIsValid(childtup = systable_getnext(pscan))) - { - Form_pg_constraint childcon; - Relation childrel; - - childcon = (Form_pg_constraint) GETSTRUCT(childtup); - - /* - * If the child constraint has already been validated, no further - * action is required for it or its descendants, as they are all - * valid. - */ - if (childcon->convalidated) - continue; - - childrel = table_open(childcon->conrelid, lockmode); - - /* - * NB: Note that pkrelid should be passed as-is during recursion, - * as it is required to identify the root referenced table. - */ - QueueFKConstraintValidation(wqueue, conrel, childrel, pkrelid, - childtup, lockmode); - table_close(childrel, NoLock); - } - - systable_endscan(pscan); - } ++ CacheInvalidateRelcache(rel); + + /* - * Now mark the pg_constraint row as validated (even if we didn't check, - * notably the ones for partitions on the referenced side). - * - * We rely on transaction abort to roll back this change if phase 3 - * ultimately finds violating rows. This is a bit ugly. ++ * Now update the catalogs, while we have the door open. + */ + copyTuple = heap_copytuple(contuple); + copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); + copy_con->convalidated = true; + CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple); + + InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0); + + heap_freetuple(copyTuple); } /* - * QueueCheckConstraintValidation - * CreateFKCheckTrigger - * Creates the insert (on_insert=true) or update "check" trigger that - * implements a given foreign key ++ * transformColumnNameList - transform list of column names * - * Add an entry to the wqueue to validate the given check constraint in Phase 3 - * and update the convalidated field in the pg_constraint catalog for the - * specified relation and all its inheriting children. - * Returns the OID of the so created trigger. ++ * Lookup each name and return its attnum and, optionally, type and collation ++ * OIDs ++ * ++ * Note: the name of this function suggests that it's general-purpose, ++ * but actually it's only used to look up names appearing in foreign-key ++ * clauses. The error messages would need work to use it in other cases, ++ * and perhaps the validity checks as well. */ - static void - QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel, - char *constrName, HeapTuple contuple, - bool recurse, bool recursing, LOCKMODE lockmode) -static Oid -CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint, - Oid constraintOid, Oid indexOid, Oid parentTrigOid, - bool on_insert) ++static int ++transformColumnNameList(Oid relId, List *colList, ++ int16 *attnums, Oid *atttypids, Oid *attcollids) { - Form_pg_constraint con; - AlteredTableInfo *tab; - HeapTuple copyTuple; - Form_pg_constraint copy_con; - - List *children = NIL; - ListCell *child; - NewConstraint *newcon; - Datum val; - char *conbin; - - con = (Form_pg_constraint) GETSTRUCT(contuple); - Assert(con->contype == CONSTRAINT_CHECK); - - /* - * If we're recursing, the parent has already done this, so skip it. Also, - * if the constraint is a NO INHERIT constraint, we shouldn't try to look - * for it in the children. - */ - if (!recursing && !con->connoinherit) - children = find_all_inheritors(RelationGetRelid(rel), - lockmode, NULL); - - /* - * For CHECK constraints, we must ensure that we only mark the constraint - * as validated on the parent if it's already validated on the children. - * - * We recurse before validating on the parent, to reduce risk of - * deadlocks. - */ - foreach(child, children) - { - Oid childoid = lfirst_oid(child); - Relation childrel; - - if (childoid == RelationGetRelid(rel)) - continue; - - /* - * If we are told not to recurse, there had better not be any child - * tables, because we can't mark the constraint on the parent valid - * unless it is valid for all child tables. - */ - if (!recurse) - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("constraint must be validated on child tables too"))); - - /* find_all_inheritors already got lock */ - childrel = table_open(childoid, NoLock); - - ATExecValidateConstraint(wqueue, childrel, constrName, false, - true, lockmode); - table_close(childrel, NoLock); - } - - /* Queue validation for phase 3 */ - newcon = palloc0_object(NewConstraint); - newcon->name = constrName; - newcon->contype = CONSTR_CHECK; - newcon->refrelid = InvalidOid; - newcon->refindid = InvalidOid; - newcon->conid = con->oid; - - val = SysCacheGetAttrNotNull(CONSTROID, contuple, - Anum_pg_constraint_conbin); - conbin = TextDatumGetCString(val); - newcon->qual = expand_generated_columns_in_expr(stringToNode(conbin), rel, 1); - - /* Find or create work queue entry for this table */ - tab = ATGetQueueEntry(wqueue, rel); - tab->constraints = lappend(tab->constraints, newcon); - - /* - * Invalidate relcache so that others see the new validated constraint. - */ - CacheInvalidateRelcache(rel); - - /* - * Now update the catalog, while we have the door open. - */ - copyTuple = heap_copytuple(contuple); - copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); - copy_con->convalidated = true; - CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple); - - InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0); - - heap_freetuple(copyTuple); - } - - /* - * QueueNNConstraintValidation - * - * Add an entry to the wqueue to validate the given not-null constraint in - * Phase 3 and update the convalidated field in the pg_constraint catalog for - * the specified relation and all its inheriting children. - */ - static void - QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel, - HeapTuple contuple, bool recurse, bool recursing, - LOCKMODE lockmode) - { - Form_pg_constraint con; - AlteredTableInfo *tab; - HeapTuple copyTuple; - Form_pg_constraint copy_con; - List *children = NIL; - AttrNumber attnum; - char *colname; - - con = (Form_pg_constraint) GETSTRUCT(contuple); - Assert(con->contype == CONSTRAINT_NOTNULL); - - attnum = extractNotNullColumn(contuple); - - /* - * If we're recursing, we've already done this for parent, so skip it. - * Also, if the constraint is a NO INHERIT constraint, we shouldn't try to - * look for it in the children. - * - * We recurse before validating on the parent, to reduce risk of - * deadlocks. - */ - if (!recursing && !con->connoinherit) - children = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL); - - colname = get_attname(RelationGetRelid(rel), attnum, false); - foreach_oid(childoid, children) - { - Relation childrel; - HeapTuple contup; - Form_pg_constraint childcon; - char *conname; - - if (childoid == RelationGetRelid(rel)) - continue; - - /* - * If we are told not to recurse, there had better not be any child - * tables, because we can't mark the constraint on the parent valid - * unless it is valid for all child tables. - */ - if (!recurse) - ereport(ERROR, - errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("constraint must be validated on child tables too")); - - /* - * The column on child might have a different attnum, so search by - * column name. - */ - contup = findNotNullConstraint(childoid, colname); - if (!contup) - elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"", - colname, get_rel_name(childoid)); - childcon = (Form_pg_constraint) GETSTRUCT(contup); - if (childcon->convalidated) - continue; - - /* find_all_inheritors already got lock */ - childrel = table_open(childoid, NoLock); - conname = pstrdup(NameStr(childcon->conname)); - - /* XXX improve ATExecValidateConstraint API to avoid double search */ - ATExecValidateConstraint(wqueue, childrel, conname, - false, true, lockmode); - table_close(childrel, NoLock); - } - - /* Set attnotnull appropriately without queueing another validation */ - set_attnotnull(NULL, rel, attnum, true, false); - - tab = ATGetQueueEntry(wqueue, rel); - tab->verify_new_notnull = true; - - /* - * Invalidate relcache so that others see the new validated constraint. - */ - CacheInvalidateRelcache(rel); - ObjectAddress trigAddress; - CreateTrigStmt *fk_trigger; -- -- /* - * Now update the catalogs, while we have the door open. - * Note: for a self-referential FK (referencing and referenced tables are - * the same), it is important that the ON UPDATE action fires before the - * CHECK action, since both triggers will fire on the same row during an - * UPDATE event; otherwise the CHECK trigger will be checking a non-final - * state of the row. Triggers fire in name order, so we ensure this by - * using names like "RI_ConstraintTrigger_a_NNNN" for the action triggers - * and "RI_ConstraintTrigger_c_NNNN" for the check triggers. -- */ - copyTuple = heap_copytuple(contuple); - copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); - copy_con->convalidated = true; - CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple); - - InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0); - - heap_freetuple(copyTuple); - } - - /* - * transformColumnNameList - transform list of column names - * - * Lookup each name and return its attnum and, optionally, type and collation - * OIDs - * - * Note: the name of this function suggests that it's general-purpose, - * but actually it's only used to look up names appearing in foreign-key - * clauses. The error messages would need work to use it in other cases, - * and perhaps the validity checks as well. - */ - static int - transformColumnNameList(Oid relId, List *colList, - int16 *attnums, Oid *atttypids, Oid *attcollids) - { - ListCell *l; - int attnum; - fk_trigger = makeNode(CreateTrigStmt); - fk_trigger->replace = false; - fk_trigger->isconstraint = true; - fk_trigger->trigname = "RI_ConstraintTrigger_c"; - fk_trigger->relation = NULL; ++ ListCell *l; ++ int attnum; - /* Either ON INSERT or ON UPDATE */ - if (on_insert) - { - fk_trigger->funcname = SystemFuncName("RI_FKey_check_ins"); - fk_trigger->events = TRIGGER_TYPE_INSERT; - } - else + attnum = 0; + foreach(l, colList) { - fk_trigger->funcname = SystemFuncName("RI_FKey_check_upd"); - fk_trigger->events = TRIGGER_TYPE_UPDATE; - } - - fk_trigger->args = NIL; - fk_trigger->row = true; - fk_trigger->timing = TRIGGER_TYPE_AFTER; - fk_trigger->columns = NIL; - fk_trigger->whenClause = NULL; - fk_trigger->transitionRels = NIL; - fk_trigger->deferrable = fkconstraint->deferrable; - fk_trigger->initdeferred = fkconstraint->initdeferred; - fk_trigger->constrrel = NULL; - - trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid, - constraintOid, indexOid, InvalidOid, - parentTrigOid, NULL, true, false); + char *attname = strVal(lfirst(l)); + HeapTuple atttuple; + Form_pg_attribute attform; - /* Make changes-so-far visible */ - CommandCounterIncrement(); + atttuple = SearchSysCacheAttName(relId, attname); + if (!HeapTupleIsValid(atttuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" referenced in foreign key constraint does not exist", + attname))); + attform = (Form_pg_attribute) GETSTRUCT(atttuple); + if (attform->attnum < 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("system columns cannot be used in foreign keys"))); + if (attnum >= INDEX_MAX_KEYS) + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_COLUMNS), + errmsg("cannot have more than %d keys in a foreign key", + INDEX_MAX_KEYS))); + attnums[attnum] = attform->attnum; + if (atttypids != NULL) + atttypids[attnum] = attform->atttypid; + if (attcollids != NULL) + attcollids[attnum] = attform->attcollation; + ReleaseSysCache(atttuple); + attnum++; + } - return trigAddress.objectId; + return attnum; } /* @@@ -14383,3786 -13214,312 +13572,3787 @@@ CreateFKCheckTrigger(Oid myRelOid, Oid } /* - * ALTER COLUMN TYPE - * - * Unlike other subcommand types, we do parse transformation for ALTER COLUMN - * TYPE during phase 1 --- the AlterTableCmd passed in here is already - * transformed (and must be, because we rely on some transformed fields). - * - * The point of this is that the execution of all ALTER COLUMN TYPEs for a - * table will be done "in parallel" during phase 3, so all the USING - * expressions should be parsed assuming the original column types. Also, - * this allows a USING expression to refer to a field that will be dropped. + * createForeignKeyActionTriggers + * Create the referenced-side "action" triggers that implement a foreign + * key. * - * To make this work safely, AT_PASS_DROP then AT_PASS_ALTER_TYPE must be - * the first two execution steps in phase 2; they must not see the effects - * of any other subcommand types, since the USING expressions are parsed - * against the unmodified table's state. + * Returns the OIDs of the so created triggers in *deleteTrigOid and + * *updateTrigOid. */ static void -ATPrepAlterColumnType(List **wqueue, - AlteredTableInfo *tab, Relation rel, - bool recurse, bool recursing, - AlterTableCmd *cmd, LOCKMODE lockmode, - AlterTableUtilityContext *context) +createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint, + Oid constraintOid, Oid indexOid, + Oid parentDelTrigger, Oid parentUpdTrigger, + Oid *deleteTrigOid, Oid *updateTrigOid) { - char *colName = cmd->name; - ColumnDef *def = (ColumnDef *) cmd->def; - TypeName *typeName = def->typeName; - Node *transform = def->cooked_default; - HeapTuple tuple; - Form_pg_attribute attTup; - AttrNumber attnum; - Oid targettype; - int32 targettypmod; - Oid targetcollid; - NewColumnValue *newval; - ParseState *pstate = make_parsestate(NULL); - AclResult aclresult; - bool is_expr; - - pstate->p_sourcetext = context->queryString; - - if (rel->rd_rel->reloftype && !recursing) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot alter column type of typed table"), - parser_errposition(pstate, def->location))); + CreateTrigStmt *fk_trigger; + ObjectAddress trigAddress; - /* lookup the attribute so we can check inheritance status */ - tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName); - if (!HeapTupleIsValid(tuple)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("column \"%s\" of relation \"%s\" does not exist", + /* + * Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON + * DELETE action on the referenced table. + */ + fk_trigger = makeNode(CreateTrigStmt); + fk_trigger->replace = false; + fk_trigger->isconstraint = true; + fk_trigger->trigname = "RI_ConstraintTrigger_a"; + fk_trigger->relation = NULL; + fk_trigger->args = NIL; + fk_trigger->row = true; + fk_trigger->timing = TRIGGER_TYPE_AFTER; + fk_trigger->events = TRIGGER_TYPE_DELETE; + fk_trigger->columns = NIL; + fk_trigger->whenClause = NULL; + fk_trigger->transitionRels = NIL; + fk_trigger->constrrel = NULL; + + switch (fkconstraint->fk_del_action) + { + case FKCONSTR_ACTION_NOACTION: + fk_trigger->deferrable = fkconstraint->deferrable; + fk_trigger->initdeferred = fkconstraint->initdeferred; + fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_del"); + break; + case FKCONSTR_ACTION_RESTRICT: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_del"); + break; + case FKCONSTR_ACTION_CASCADE: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_del"); + break; + case FKCONSTR_ACTION_SETNULL: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_del"); + break; + case FKCONSTR_ACTION_SETDEFAULT: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_del"); + break; + default: + elog(ERROR, "unrecognized FK action type: %d", + (int) fkconstraint->fk_del_action); + break; + } + + trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid, + constraintOid, indexOid, InvalidOid, + parentDelTrigger, NULL, true, false); + if (deleteTrigOid) + *deleteTrigOid = trigAddress.objectId; + + /* Make changes-so-far visible */ + CommandCounterIncrement(); + + /* + * Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON + * UPDATE action on the referenced table. + */ + fk_trigger = makeNode(CreateTrigStmt); + fk_trigger->replace = false; + fk_trigger->isconstraint = true; + fk_trigger->trigname = "RI_ConstraintTrigger_a"; + fk_trigger->relation = NULL; + fk_trigger->args = NIL; + fk_trigger->row = true; + fk_trigger->timing = TRIGGER_TYPE_AFTER; + fk_trigger->events = TRIGGER_TYPE_UPDATE; + fk_trigger->columns = NIL; + fk_trigger->whenClause = NULL; + fk_trigger->transitionRels = NIL; + fk_trigger->constrrel = NULL; + + switch (fkconstraint->fk_upd_action) + { + case FKCONSTR_ACTION_NOACTION: + fk_trigger->deferrable = fkconstraint->deferrable; + fk_trigger->initdeferred = fkconstraint->initdeferred; + fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_upd"); + break; + case FKCONSTR_ACTION_RESTRICT: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_upd"); + break; + case FKCONSTR_ACTION_CASCADE: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_upd"); + break; + case FKCONSTR_ACTION_SETNULL: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_upd"); + break; + case FKCONSTR_ACTION_SETDEFAULT: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_upd"); + break; + default: + elog(ERROR, "unrecognized FK action type: %d", + (int) fkconstraint->fk_upd_action); + break; + } + + trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid, + constraintOid, indexOid, InvalidOid, + parentUpdTrigger, NULL, true, false); + if (updateTrigOid) + *updateTrigOid = trigAddress.objectId; +} + +/* + * createForeignKeyCheckTriggers + * Create the referencing-side "check" triggers that implement a foreign + * key. + * + * Returns the OIDs of the so created triggers in *insertTrigOid and + * *updateTrigOid. + */ +static void +createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid, + Constraint *fkconstraint, Oid constraintOid, + Oid indexOid, + Oid parentInsTrigger, Oid parentUpdTrigger, + Oid *insertTrigOid, Oid *updateTrigOid) +{ + *insertTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, + constraintOid, indexOid, + parentInsTrigger, true); + *updateTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, + constraintOid, indexOid, + parentUpdTrigger, false); +} + +/* + * ALTER TABLE DROP CONSTRAINT + * + * Like DROP COLUMN, we can't use the normal ALTER TABLE recursion mechanism. + */ +static void +ATExecDropConstraint(Relation rel, const char *constrName, + DropBehavior behavior, bool recurse, + bool missing_ok, LOCKMODE lockmode) +{ + Relation conrel; + SysScanDesc scan; + ScanKeyData skey[3]; + HeapTuple tuple; + bool found = false; + + conrel = table_open(ConstraintRelationId, RowExclusiveLock); + + /* + * Find and drop the target constraint + */ + ScanKeyInit(&skey[0], + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(rel))); + ScanKeyInit(&skey[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + ScanKeyInit(&skey[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(constrName)); + scan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId, + true, NULL, 3, skey); + + /* There can be at most one matching row */ + if (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + dropconstraint_internal(rel, tuple, behavior, recurse, false, + missing_ok, lockmode); + found = true; + } + + systable_endscan(scan); + + if (!found) + { + if (!missing_ok) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("constraint \"%s\" of relation \"%s\" does not exist", + constrName, RelationGetRelationName(rel))); + else + ereport(NOTICE, + errmsg("constraint \"%s\" of relation \"%s\" does not exist, skipping", + constrName, RelationGetRelationName(rel))); + } + + table_close(conrel, RowExclusiveLock); +} + +/* + * Remove a constraint, using its pg_constraint tuple + * + * Implementation for ALTER TABLE DROP CONSTRAINT and ALTER TABLE ALTER COLUMN + * DROP NOT NULL. + * + * Returns the address of the constraint being removed. + */ +static ObjectAddress +dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior behavior, + bool recurse, bool recursing, bool missing_ok, + LOCKMODE lockmode) +{ + Relation conrel; + Form_pg_constraint con; + ObjectAddress conobj; + List *children; + bool is_no_inherit_constraint = false; + char *constrName; + char *colname = NULL; + + /* Guard against stack overflow due to overly deep inheritance tree. */ + check_stack_depth(); + + /* At top level, permission check was done in ATPrepCmd, else do it */ + if (recursing) + ATSimplePermissions(AT_DropConstraint, rel, + ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); + + conrel = table_open(ConstraintRelationId, RowExclusiveLock); + + con = (Form_pg_constraint) GETSTRUCT(constraintTup); + constrName = NameStr(con->conname); + + /* Don't allow drop of inherited constraints */ + if (con->coninhcount > 0 && !recursing) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("cannot drop inherited constraint \"%s\" of relation \"%s\"", + constrName, RelationGetRelationName(rel)))); + + /* + * Reset pg_constraint.attnotnull, if this is a not-null constraint. + * + * While doing that, we're in a good position to disallow dropping a not- + * null constraint underneath a primary key, a replica identity index, or + * a generated identity column. + */ + if (con->contype == CONSTRAINT_NOTNULL) + { + Relation attrel = table_open(AttributeRelationId, RowExclusiveLock); + AttrNumber attnum = extractNotNullColumn(constraintTup); + Bitmapset *pkattrs; + Bitmapset *irattrs; + HeapTuple atttup; + Form_pg_attribute attForm; + + /* save column name for recursion step */ + colname = get_attname(RelationGetRelid(rel), attnum, false); + + /* + * Disallow if it's in the primary key. For partitioned tables we + * cannot rely solely on RelationGetIndexAttrBitmap, because it'll + * return NULL if the primary key is invalid; but we still need to + * protect not-null constraints under such a constraint, so check the + * slow way. + */ + pkattrs = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_PRIMARY_KEY); + + if (pkattrs == NULL && + rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { + Oid pkindex = RelationGetPrimaryKeyIndex(rel, true); + + if (OidIsValid(pkindex)) + { + Relation pk = relation_open(pkindex, AccessShareLock); + + pkattrs = NULL; + for (int i = 0; i < pk->rd_index->indnkeyatts; i++) + pkattrs = bms_add_member(pkattrs, pk->rd_index->indkey.values[i]); + + relation_close(pk, AccessShareLock); + } + } + + if (pkattrs && + bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, pkattrs)) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("column \"%s\" is in a primary key", + get_attname(RelationGetRelid(rel), attnum, false))); + + /* Disallow if it's in the replica identity */ + irattrs = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_IDENTITY_KEY); + if (bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, irattrs)) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("column \"%s\" is in index used as replica identity", + get_attname(RelationGetRelid(rel), attnum, false))); + + /* Disallow if it's a GENERATED AS IDENTITY column */ + atttup = SearchSysCacheCopyAttNum(RelationGetRelid(rel), attnum); + if (!HeapTupleIsValid(atttup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + attnum, RelationGetRelid(rel)); + attForm = (Form_pg_attribute) GETSTRUCT(atttup); + if (attForm->attidentity != '\0') + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("column \"%s\" of relation \"%s\" is an identity column", + get_attname(RelationGetRelid(rel), attnum, + false), + RelationGetRelationName(rel))); + + /* All good -- reset attnotnull if needed */ + if (attForm->attnotnull) + { + attForm->attnotnull = false; + CatalogTupleUpdate(attrel, &atttup->t_self, atttup); + } + + table_close(attrel, RowExclusiveLock); + } + + is_no_inherit_constraint = con->connoinherit; + + /* + * If it's a foreign-key constraint, we'd better lock the referenced table + * and check that that's not in use, just as we've already done for the + * constrained table (else we might, eg, be dropping a trigger that has + * unfired events). But we can/must skip that in the self-referential + * case. + */ + if (con->contype == CONSTRAINT_FOREIGN && + con->confrelid != RelationGetRelid(rel)) + { + Relation frel; + + /* Must match lock taken by RemoveTriggerById: */ + frel = table_open(con->confrelid, AccessExclusiveLock); + CheckAlterTableIsSafe(frel); + table_close(frel, NoLock); + } + + /* + * Perform the actual constraint deletion + */ + ObjectAddressSet(conobj, ConstraintRelationId, con->oid); + performDeletion(&conobj, behavior, 0); + + /* + * For partitioned tables, non-CHECK, non-NOT-NULL inherited constraints + * are dropped via the dependency mechanism, so we're done here. + */ + if (con->contype != CONSTRAINT_CHECK && + con->contype != CONSTRAINT_NOTNULL && + rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { + table_close(conrel, RowExclusiveLock); + return conobj; + } + + /* + * Propagate to children as appropriate. Unlike most other ALTER + * routines, we have to do this one level of recursion at a time; we can't + * use find_all_inheritors to do it in one pass. + */ + if (!is_no_inherit_constraint) + children = find_inheritance_children(RelationGetRelid(rel), lockmode); + else + children = NIL; + + foreach_oid(childrelid, children) + { + Relation childrel; + HeapTuple tuple; + Form_pg_constraint childcon; + + /* find_inheritance_children already got lock */ + childrel = table_open(childrelid, NoLock); + CheckAlterTableIsSafe(childrel); + + /* + * We search for not-null constraints by column name, and others by + * constraint name. + */ + if (con->contype == CONSTRAINT_NOTNULL) + { + tuple = findNotNullConstraint(childrelid, colname); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u", + colname, RelationGetRelid(childrel)); + } + else + { + SysScanDesc scan; + ScanKeyData skey[3]; + + ScanKeyInit(&skey[0], + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(childrelid)); + ScanKeyInit(&skey[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + ScanKeyInit(&skey[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(constrName)); + scan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId, + true, NULL, 3, skey); + /* There can only be one, so no need to loop */ + tuple = systable_getnext(scan); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("constraint \"%s\" of relation \"%s\" does not exist", + constrName, + RelationGetRelationName(childrel)))); + tuple = heap_copytuple(tuple); + systable_endscan(scan); + } + + childcon = (Form_pg_constraint) GETSTRUCT(tuple); + + /* Right now only CHECK and not-null constraints can be inherited */ + if (childcon->contype != CONSTRAINT_CHECK && + childcon->contype != CONSTRAINT_NOTNULL) + elog(ERROR, "inherited constraint is not a CHECK or not-null constraint"); + + if (childcon->coninhcount <= 0) /* shouldn't happen */ + elog(ERROR, "relation %u has non-inherited constraint \"%s\"", + childrelid, NameStr(childcon->conname)); + + if (recurse) + { + /* + * If the child constraint has other definition sources, just + * decrement its inheritance count; if not, recurse to delete it. + */ + if (childcon->coninhcount == 1 && !childcon->conislocal) + { + /* Time to delete this child constraint, too */ + dropconstraint_internal(childrel, tuple, behavior, + recurse, true, missing_ok, + lockmode); + } + else + { + /* Child constraint must survive my deletion */ + childcon->coninhcount--; + CatalogTupleUpdate(conrel, &tuple->t_self, tuple); + + /* Make update visible */ + CommandCounterIncrement(); + } + } + else + { + /* + * If we were told to drop ONLY in this table (no recursion) and + * there are no further parents for this constraint, we need to + * mark the inheritors' constraints as locally defined rather than + * inherited. + */ + childcon->coninhcount--; + if (childcon->coninhcount == 0) + childcon->conislocal = true; + + CatalogTupleUpdate(conrel, &tuple->t_self, tuple); + + /* Make update visible */ + CommandCounterIncrement(); + } + + heap_freetuple(tuple); + + table_close(childrel, NoLock); + } + + table_close(conrel, RowExclusiveLock); + + return conobj; +} + +/* + * ALTER COLUMN TYPE + * + * Unlike other subcommand types, we do parse transformation for ALTER COLUMN + * TYPE during phase 1 --- the AlterTableCmd passed in here is already + * transformed (and must be, because we rely on some transformed fields). + * + * The point of this is that the execution of all ALTER COLUMN TYPEs for a + * table will be done "in parallel" during phase 3, so all the USING + * expressions should be parsed assuming the original column types. Also, + * this allows a USING expression to refer to a field that will be dropped. + * + * To make this work safely, AT_PASS_DROP then AT_PASS_ALTER_TYPE must be + * the first two execution steps in phase 2; they must not see the effects + * of any other subcommand types, since the USING expressions are parsed + * against the unmodified table's state. + */ +static void +ATPrepAlterColumnType(List **wqueue, + AlteredTableInfo *tab, Relation rel, + bool recurse, bool recursing, + AlterTableCmd *cmd, LOCKMODE lockmode, + AlterTableUtilityContext *context) +{ + char *colName = cmd->name; + ColumnDef *def = (ColumnDef *) cmd->def; + TypeName *typeName = def->typeName; + Node *transform = def->cooked_default; + HeapTuple tuple; + Form_pg_attribute attTup; + AttrNumber attnum; + Oid targettype; + int32 targettypmod; + Oid targetcollid; + NewColumnValue *newval; + ParseState *pstate = make_parsestate(NULL); + AclResult aclresult; + bool is_expr; + + pstate->p_sourcetext = context->queryString; + + if (rel->rd_rel->reloftype && !recursing) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot alter column type of typed table"), + parser_errposition(pstate, def->location))); + + /* lookup the attribute so we can check inheritance status */ + tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + colName, RelationGetRelationName(rel)), + parser_errposition(pstate, def->location))); + attTup = (Form_pg_attribute) GETSTRUCT(tuple); + attnum = attTup->attnum; + + /* Can't alter a system attribute */ + if (attnum <= 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter system column \"%s\"", colName), + parser_errposition(pstate, def->location))); + + /* + * 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), + parser_errposition(pstate, def->location))); + + /* + * 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). + */ + if (attTup->attinhcount > 0 && !recursing) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("cannot alter inherited column \"%s\"", colName), + parser_errposition(pstate, def->location))); + + /* Don't alter columns used in the partition key */ + if (has_partition_attrs(rel, + bms_make_singleton(attnum - FirstLowInvalidHeapAttributeNumber), + &is_expr)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("cannot alter column \"%s\" because it is part of the partition key of relation \"%s\"", colName, RelationGetRelationName(rel)), parser_errposition(pstate, def->location))); - attTup = (Form_pg_attribute) GETSTRUCT(tuple); + + /* Look up the target type */ + typenameTypeIdAndMod(pstate, typeName, &targettype, &targettypmod); + + aclresult = object_aclcheck(TypeRelationId, targettype, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error_type(aclresult, targettype); + + /* And the collation */ + targetcollid = GetColumnDefCollation(pstate, def, targettype); + + /* make sure datatype is legal for a column */ + CheckAttributeType(colName, targettype, targetcollid, + list_make1_oid(rel->rd_rel->reltype), + (attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL ? CHKATYPE_IS_VIRTUAL : 0)); + + if (attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) + { + /* do nothing */ + } + else if (tab->relkind == RELKIND_RELATION || + tab->relkind == RELKIND_PARTITIONED_TABLE) + { + /* + * Set up an expression to transform the old data value to the new + * type. If a USING option was given, use the expression as + * transformed by transformAlterTableStmt, else just take the old + * value and try to coerce it. We do this first so that type + * incompatibility can be detected before we waste effort, and because + * we need the expression to be parsed against the original table row + * type. + */ + if (!transform) + { + transform = (Node *) makeVar(1, attnum, + attTup->atttypid, attTup->atttypmod, + attTup->attcollation, + 0); + } + + transform = coerce_to_target_type(pstate, + transform, exprType(transform), + targettype, targettypmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + -1); + if (transform == NULL) + { + /* error text depends on whether USING was specified or not */ + if (def->cooked_default != NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("result of USING clause for column \"%s\"" + " cannot be cast automatically to type %s", + colName, format_type_be(targettype)), + errhint("You might need to add an explicit cast."))); + else + ereport(ERROR, + (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)) : 0)); + } + + /* Fix collations after all else */ + assign_expr_collations(pstate, transform); + + /* Expand virtual generated columns in the expr. */ + transform = expand_generated_columns_in_expr(transform, rel, 1); + + /* Plan the expr now so we can accurately assess the need to rewrite. */ + transform = (Node *) expression_planner((Expr *) transform); + + /* + * Add a work queue item to make ATRewriteTable update the column + * contents. + */ + newval = palloc0_object(NewColumnValue); + newval->attnum = attnum; + newval->expr = (Expr *) transform; + newval->is_generated = false; + + tab->newvals = lappend(tab->newvals, newval); + if (ATColumnChangeRequiresRewrite(transform, attnum)) + tab->rewrite |= AT_REWRITE_COLUMN_REWRITE; + } + else if (transform) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a table", + RelationGetRelationName(rel)))); + + if (!RELKIND_HAS_STORAGE(tab->relkind) || attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) + { + /* + * For relations or columns without storage, do this check now. + * Regular tables will check it later when the table is being + * rewritten. + */ + find_composite_type_dependencies(rel->rd_rel->reltype, rel, NULL); + } + + ReleaseSysCache(tuple); + + /* + * Recurse manually by queueing a new command for each child, if + * necessary. We cannot apply ATSimpleRecursion here because we need to + * remap attribute numbers in the USING expression, if any. + * + * If we are told not to recurse, there had better not be any child + * tables; else the alter would put them out of step. + */ + if (recurse) + { + Oid relid = RelationGetRelid(rel); + List *child_oids, + *child_numparents; + ListCell *lo, + *li; + + child_oids = find_all_inheritors(relid, lockmode, + &child_numparents); + + /* + * find_all_inheritors does the recursive search of the inheritance + * hierarchy, so all we have to do is process all of the relids in the + * list that it returns. + */ + forboth(lo, child_oids, li, child_numparents) + { + Oid childrelid = lfirst_oid(lo); + int numparents = lfirst_int(li); + Relation childrel; + HeapTuple childtuple; + Form_pg_attribute childattTup; + + if (childrelid == relid) + continue; + + /* find_all_inheritors already got lock */ + childrel = relation_open(childrelid, NoLock); + CheckAlterTableIsSafe(childrel); + + /* + * Verify that the child doesn't have any inherited definitions of + * this column that came from outside this inheritance hierarchy. + * (renameatt makes a similar test, though in a different way + * because of its different recursion mechanism.) + */ + childtuple = SearchSysCacheAttName(RelationGetRelid(childrel), + colName); + if (!HeapTupleIsValid(childtuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + colName, RelationGetRelationName(childrel)))); + childattTup = (Form_pg_attribute) GETSTRUCT(childtuple); + + if (childattTup->attinhcount > numparents) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("cannot alter inherited column \"%s\" of relation \"%s\"", + colName, RelationGetRelationName(childrel)))); + + ReleaseSysCache(childtuple); + + /* + * Remap the attribute numbers. If no USING expression was + * specified, there is no need for this step. + */ + if (def->cooked_default) + { + AttrMap *attmap; + bool found_whole_row; + + /* create a copy to scribble on */ + cmd = copyObject(cmd); + + attmap = build_attrmap_by_name(RelationGetDescr(childrel), + RelationGetDescr(rel), + false); + ((ColumnDef *) cmd->def)->cooked_default = + map_variable_attnos(def->cooked_default, + 1, 0, + attmap, + InvalidOid, &found_whole_row); + if (found_whole_row) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot convert whole-row table reference"), + errdetail("USING expression contains a whole-row table reference."))); + pfree(attmap); + } + ATPrepCmd(wqueue, childrel, cmd, false, true, lockmode, context); + relation_close(childrel, NoLock); + } + } + else if (!recursing && + find_inheritance_children(RelationGetRelid(rel), NoLock) != NIL) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("type of inherited column \"%s\" must be changed in child tables too", + colName))); + + if (tab->relkind == RELKIND_COMPOSITE_TYPE) + ATTypedTableRecursion(wqueue, rel, cmd, lockmode, context); +} + +/* + * When the data type of a column is changed, a rewrite might not be required + * if the new type is sufficiently identical to the old one, and the USING + * clause isn't trying to insert some other value. It's safe to skip the + * rewrite in these cases: + * + * - the old type is binary coercible to the new type + * - the new type is an unconstrained domain over the old type + * - {NEW,OLD} or {OLD,NEW} is {timestamptz,timestamp} and the timezone is UTC + * + * In the case of a constrained domain, we could get by with scanning the + * table and checking the constraint rather than actually rewriting it, but we + * don't currently try to do that. + */ +static bool +ATColumnChangeRequiresRewrite(Node *expr, AttrNumber varattno) +{ + Assert(expr != NULL); + + for (;;) + { + /* only one varno, so no need to check that */ + if (IsA(expr, Var) && ((Var *) expr)->varattno == varattno) + return false; + else if (IsA(expr, RelabelType)) + expr = (Node *) ((RelabelType *) expr)->arg; + else if (IsA(expr, CoerceToDomain)) + { + CoerceToDomain *d = (CoerceToDomain *) expr; + + if (DomainHasConstraints(d->resulttype, NULL)) + return true; + expr = (Node *) d->arg; + } + else if (IsA(expr, FuncExpr)) + { + FuncExpr *f = (FuncExpr *) expr; + + switch (f->funcid) + { + case F_TIMESTAMPTZ_TIMESTAMP: + case F_TIMESTAMP_TIMESTAMPTZ: + if (TimestampTimestampTzRequiresRewrite()) + return true; + else + expr = linitial(f->args); + break; + default: + return true; + } + } + else + return true; + } +} + +/* + * ALTER COLUMN .. SET DATA TYPE + * + * Return the address of the modified column. + */ +static ObjectAddress +ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, + AlterTableCmd *cmd, LOCKMODE lockmode) +{ + char *colName = cmd->name; + ColumnDef *def = (ColumnDef *) cmd->def; + TypeName *typeName = def->typeName; + HeapTuple heapTup; + Form_pg_attribute attTup, + attOldTup; + AttrNumber attnum; + HeapTuple typeTuple; + Form_pg_type tform; + Oid targettype; + int32 targettypmod; + Oid targetcollid; + Node *defaultexpr; + Relation attrelation; + Relation depRel; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple depTup; + ObjectAddress address; + + /* + * Clear all the missing values if we're rewriting the table, since this + * renders them pointless. + */ + if (tab->rewrite) + { + Relation newrel; + + newrel = table_open(RelationGetRelid(rel), NoLock); + RelationClearMissing(newrel); + relation_close(newrel, NoLock); + /* make sure we don't conflict with later attribute modifications */ + CommandCounterIncrement(); + } + + attrelation = table_open(AttributeRelationId, RowExclusiveLock); + + /* Look up the target column */ + heapTup = SearchSysCacheCopyAttName(RelationGetRelid(rel), colName); + if (!HeapTupleIsValid(heapTup)) /* shouldn't happen */ + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + colName, RelationGetRelationName(rel)))); + attTup = (Form_pg_attribute) GETSTRUCT(heapTup); attnum = attTup->attnum; + attOldTup = TupleDescAttr(tab->oldDesc, attnum - 1); + + /* Check for multiple ALTER TYPE on same column --- can't cope */ + if (attTup->atttypid != attOldTup->atttypid || + attTup->atttypmod != attOldTup->atttypmod) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type of column \"%s\" twice", + colName))); + + /* Look up the target type (should not fail, since prep found it) */ + typeTuple = typenameType(NULL, typeName, &targettypmod); + tform = (Form_pg_type) GETSTRUCT(typeTuple); + targettype = tform->oid; + /* And the collation */ + targetcollid = GetColumnDefCollation(NULL, def, targettype); + + /* + * If there is a default expression for the column, get it and ensure we + * can coerce it to the new datatype. (We must do this before changing + * the column type, because build_column_default itself will try to + * coerce, and will not issue the error message we want if it fails.) + * + * We remove any implicit coercion steps at the top level of the old + * default expression; this has been agreed to satisfy the principle of + * least surprise. (The conversion to the new column type should act like + * it started from what the user sees as the stored expression, and the + * implicit coercions aren't going to be shown.) + */ + if (attTup->atthasdef) + { + defaultexpr = build_column_default(rel, attnum); + Assert(defaultexpr); + defaultexpr = strip_implicit_coercions(defaultexpr); + defaultexpr = coerce_to_target_type(NULL, /* no UNKNOWN params */ + defaultexpr, exprType(defaultexpr), + targettype, targettypmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + -1); + if (defaultexpr == NULL) + { + if (attTup->attgenerated) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("generation expression for column \"%s\" cannot be cast automatically to type %s", + colName, format_type_be(targettype)))); + else + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("default for column \"%s\" cannot be cast automatically to type %s", + colName, format_type_be(targettype)))); + } + } + else + defaultexpr = NULL; + + /* + * Find everything that depends on the column (constraints, indexes, etc), + * and record enough information to let us recreate the objects. + * + * The actual recreation does not happen here, but only after we have + * performed all the individual ALTER TYPE operations. We have to save + * the info before executing ALTER TYPE, though, else the deparser will + * get confused. + */ + RememberAllDependentForRebuilding(tab, AT_AlterColumnType, rel, attnum, colName); + + /* + * Now scan for dependencies of this column on other things. The only + * things we should find are the dependency on the column datatype and + * possibly a collation dependency. Those can be removed. + */ + depRel = table_open(DependRelationId, RowExclusiveLock); + + ScanKeyInit(&key[0], + Anum_pg_depend_classid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationRelationId)); + ScanKeyInit(&key[1], + Anum_pg_depend_objid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(rel))); + ScanKeyInit(&key[2], + Anum_pg_depend_objsubid, + BTEqualStrategyNumber, F_INT4EQ, + Int32GetDatum((int32) attnum)); + + scan = systable_beginscan(depRel, DependDependerIndexId, true, + NULL, 3, key); + + while (HeapTupleIsValid(depTup = systable_getnext(scan))) + { + Form_pg_depend foundDep = (Form_pg_depend) GETSTRUCT(depTup); + ObjectAddress foundObject; + + foundObject.classId = foundDep->refclassid; + foundObject.objectId = foundDep->refobjid; + foundObject.objectSubId = foundDep->refobjsubid; + + if (foundDep->deptype != DEPENDENCY_NORMAL) + elog(ERROR, "found unexpected dependency type '%c'", + foundDep->deptype); + if (!(foundDep->refclassid == TypeRelationId && + foundDep->refobjid == attTup->atttypid) && + !(foundDep->refclassid == CollationRelationId && + foundDep->refobjid == attTup->attcollation)) + elog(ERROR, "found unexpected dependency for column: %s", + getObjectDescription(&foundObject, false)); + + CatalogTupleDelete(depRel, &depTup->t_self); + } + + systable_endscan(scan); + + table_close(depRel, RowExclusiveLock); + + /* + * Here we go --- change the recorded column type and collation. (Note + * heapTup is a copy of the syscache entry, so okay to scribble on.) First + * fix up the missing value if any. + */ + if (attTup->atthasmissing) + { + Datum missingval; + bool missingNull; + + /* if rewrite is true the missing value should already be cleared */ + Assert(tab->rewrite == 0); + + /* Get the missing value datum */ + missingval = heap_getattr(heapTup, + Anum_pg_attribute_attmissingval, + attrelation->rd_att, + &missingNull); + + /* if it's a null array there is nothing to do */ + + if (!missingNull) + { + /* + * Get the datum out of the array and repack it in a new array + * built with the new type data. We assume that since the table + * doesn't need rewriting, the actual Datum doesn't need to be + * changed, only the array metadata. + */ + + int one = 1; + bool isNull; + Datum valuesAtt[Natts_pg_attribute] = {0}; + bool nullsAtt[Natts_pg_attribute] = {0}; + bool replacesAtt[Natts_pg_attribute] = {0}; + HeapTuple newTup; + + missingval = array_get_element(missingval, + 1, + &one, + 0, + attTup->attlen, + attTup->attbyval, + attTup->attalign, + &isNull); + missingval = PointerGetDatum(construct_array(&missingval, + 1, + targettype, + tform->typlen, + tform->typbyval, + tform->typalign)); + + valuesAtt[Anum_pg_attribute_attmissingval - 1] = missingval; + replacesAtt[Anum_pg_attribute_attmissingval - 1] = true; + nullsAtt[Anum_pg_attribute_attmissingval - 1] = false; + + newTup = heap_modify_tuple(heapTup, RelationGetDescr(attrelation), + valuesAtt, nullsAtt, replacesAtt); + heap_freetuple(heapTup); + heapTup = newTup; + attTup = (Form_pg_attribute) GETSTRUCT(heapTup); + } + } + + attTup->atttypid = targettype; + attTup->atttypmod = targettypmod; + attTup->attcollation = targetcollid; + if (list_length(typeName->arrayBounds) > PG_INT16_MAX) + ereport(ERROR, + errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("too many array dimensions")); + attTup->attndims = list_length(typeName->arrayBounds); + attTup->attlen = tform->typlen; + attTup->attbyval = tform->typbyval; + attTup->attalign = tform->typalign; + attTup->attstorage = tform->typstorage; + attTup->attcompression = InvalidCompressionMethod; + + ReleaseSysCache(typeTuple); + + CatalogTupleUpdate(attrelation, &heapTup->t_self, heapTup); + + table_close(attrelation, RowExclusiveLock); + + /* Install dependencies on new datatype and collation */ + add_column_datatype_dependency(RelationGetRelid(rel), attnum, targettype); + add_column_collation_dependency(RelationGetRelid(rel), attnum, targetcollid); + + /* + * Drop any pg_statistic entry for the column, since it's now wrong type + */ + RemoveStatistics(RelationGetRelid(rel), attnum); + + InvokeObjectPostAlterHook(RelationRelationId, + RelationGetRelid(rel), attnum); + + /* + * Update the default, if present, by brute force --- remove and re-add + * the default. Probably unsafe to take shortcuts, since the new version + * may well have additional dependencies. (It's okay to do this now, + * rather than after other ALTER TYPE commands, since the default won't + * depend on other column types.) + */ + if (defaultexpr) + { + /* + * If it's a GENERATED default, drop its dependency records, in + * particular its INTERNAL dependency on the column, which would + * otherwise cause dependency.c to refuse to perform the deletion. + */ + if (attTup->attgenerated) + { + Oid attrdefoid = GetAttrDefaultOid(RelationGetRelid(rel), attnum); + + if (!OidIsValid(attrdefoid)) + elog(ERROR, "could not find attrdef tuple for relation %u attnum %d", + RelationGetRelid(rel), attnum); + (void) deleteDependencyRecordsFor(AttrDefaultRelationId, attrdefoid, false); + } + + /* + * Make updates-so-far visible, particularly the new pg_attribute row + * which will be updated again. + */ + CommandCounterIncrement(); + + /* + * We use RESTRICT here for safety, but at present we do not expect + * anything to depend on the default. + */ + RemoveAttrDefault(RelationGetRelid(rel), attnum, DROP_RESTRICT, true, + true); + + (void) StoreAttrDefault(rel, attnum, defaultexpr, true); + } + + ObjectAddressSubSet(address, RelationRelationId, + RelationGetRelid(rel), attnum); + + /* Cleanup */ + heap_freetuple(heapTup); + + return address; +} + +/* + * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything + * that depends on the column (constraints, indexes, etc), and record enough + * information to let us recreate the objects. + */ +static void +RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, + Relation rel, AttrNumber attnum, const char *colName) +{ + Relation depRel; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple depTup; + + Assert(subtype == AT_AlterColumnType || subtype == AT_SetExpression); + + depRel = table_open(DependRelationId, RowExclusiveLock); + + ScanKeyInit(&key[0], + Anum_pg_depend_refclassid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationRelationId)); + ScanKeyInit(&key[1], + Anum_pg_depend_refobjid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(rel))); + ScanKeyInit(&key[2], + Anum_pg_depend_refobjsubid, + BTEqualStrategyNumber, F_INT4EQ, + Int32GetDatum((int32) attnum)); + + scan = systable_beginscan(depRel, DependReferenceIndexId, true, + NULL, 3, key); + + while (HeapTupleIsValid(depTup = systable_getnext(scan))) + { + Form_pg_depend foundDep = (Form_pg_depend) GETSTRUCT(depTup); + ObjectAddress foundObject; + + foundObject.classId = foundDep->classid; + foundObject.objectId = foundDep->objid; + foundObject.objectSubId = foundDep->objsubid; + + switch (foundObject.classId) + { + case RelationRelationId: + { + char relKind = get_rel_relkind(foundObject.objectId); + + if (relKind == RELKIND_INDEX || + relKind == RELKIND_PARTITIONED_INDEX) + { + Assert(foundObject.objectSubId == 0); + RememberIndexForRebuilding(foundObject.objectId, tab); + } + else if (relKind == RELKIND_SEQUENCE) + { + /* + * This must be a SERIAL column's sequence. We need + * not do anything to it. + */ + Assert(foundObject.objectSubId == 0); + } + else + { + /* Not expecting any other direct dependencies... */ + elog(ERROR, "unexpected object depending on column: %s", + getObjectDescription(&foundObject, false)); + } + break; + } + + case ConstraintRelationId: + Assert(foundObject.objectSubId == 0); + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + + case ProcedureRelationId: + + /* + * A new-style SQL function can depend on a column, if that + * column is referenced in the parsed function body. Ideally + * we'd automatically update the function by deparsing and + * reparsing it, but that's risky and might well fail anyhow. + * FIXME someday. + * + * This is only a problem for AT_AlterColumnType, not + * AT_SetExpression. + */ + if (subtype == AT_AlterColumnType) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type of a column used by a function or procedure"), + errdetail("%s depends on column \"%s\"", + getObjectDescription(&foundObject, false), + colName))); + break; + + case RewriteRelationId: + + /* + * View/rule bodies have pretty much the same issues as + * function bodies. FIXME someday. + */ + if (subtype == AT_AlterColumnType) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type of a column used by a view or rule"), + errdetail("%s depends on column \"%s\"", + getObjectDescription(&foundObject, false), + colName))); + break; + + case TriggerRelationId: + + /* + * A trigger can depend on a column because the column is + * specified as an update target, or because the column is + * used in the trigger's WHEN condition. The first case would + * not require any extra work, but the second case would + * require updating the WHEN expression, which has the same + * issues as above. Since we can't easily tell which case + * applies, we punt for both. FIXME someday. + */ + if (subtype == AT_AlterColumnType) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type of a column used in a trigger definition"), + errdetail("%s depends on column \"%s\"", + getObjectDescription(&foundObject, false), + colName))); + break; + + case PolicyRelationId: + + /* + * A policy can depend on a column because the column is + * specified in the policy's USING or WITH CHECK qual + * expressions. It might be possible to rewrite and recheck + * the policy expression, but punt for now. It's certainly + * easy enough to remove and recreate the policy; still, FIXME + * someday. + */ + if (subtype == AT_AlterColumnType) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type of a column used in a policy definition"), + errdetail("%s depends on column \"%s\"", + getObjectDescription(&foundObject, false), + colName))); + break; + + case AttrDefaultRelationId: + { + ObjectAddress col = GetAttrDefaultColumnAddress(foundObject.objectId); + + if (col.objectId == RelationGetRelid(rel) && + col.objectSubId == attnum) + { + /* + * Ignore the column's own default expression. The + * caller deals with it. + */ + } + else + { + /* + * This must be a reference from the expression of a + * generated column elsewhere in the same table. + * Changing the type/generated expression of a column + * that is used by a generated column is not allowed + * by SQL standard, so just punt for now. It might be + * doable with some thinking and effort. + */ + if (subtype == AT_AlterColumnType) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type of a column used by a generated column"), + errdetail("Column \"%s\" is used by generated column \"%s\".", + colName, + get_attname(col.objectId, + col.objectSubId, + false)))); + } + break; + } + + case StatisticExtRelationId: + + /* + * Give the extended-stats machinery a chance to fix anything + * that this column type change would break. + */ + RememberStatisticsForRebuilding(foundObject.objectId, tab); + break; + + case PublicationRelRelationId: + + /* + * Column reference in a PUBLICATION ... FOR TABLE ... WHERE + * clause. Same issues as above. FIXME someday. + */ + if (subtype == AT_AlterColumnType) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type of a column used by a publication WHERE clause"), + errdetail("%s depends on column \"%s\"", + getObjectDescription(&foundObject, false), + colName))); + break; + + default: + + /* + * We don't expect any other sorts of objects to depend on a + * column. + */ + elog(ERROR, "unexpected object depending on column: %s", + getObjectDescription(&foundObject, false)); + break; + } + } + + systable_endscan(scan); + table_close(depRel, NoLock); +} + +/* + * Record information about dependencies between objects with whole-row Var + * references (indexes, check constraints, etc.) and the relation. + * + * See also RememberAllDependentForRebuilding, which handles non-whole-row Var + * references. + * + * This function currently applies only to ALTER COLUMN SET EXPRESSION. + */ +static void +RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, Relation rel) +{ + ScanKeyData skey; + Relation pg_constraint; + Relation pg_index; + SysScanDesc conscan; + SysScanDesc indscan; + HeapTuple constrTuple; + HeapTuple indexTuple; + bool isnull; + + Assert(subtype == AT_SetExpression); + + /* + * Check CHECK constraints with whole-row references first. + */ + if (RelationGetDescr(rel)->constr && + RelationGetDescr(rel)->constr->num_check > 0) + { + pg_constraint = table_open(ConstraintRelationId, AccessShareLock); + + ScanKeyInit(&skey, + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(rel))); + + conscan = systable_beginscan(pg_constraint, + ConstraintRelidTypidNameIndexId, + true, + NULL, + 1, + &skey); + + while (HeapTupleIsValid(constrTuple = systable_getnext(conscan))) + { + Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(constrTuple); + Datum exprDatum; + + if (conform->contype != CONSTRAINT_CHECK) + continue; + + exprDatum = heap_getattr(constrTuple, + Anum_pg_constraint_conbin, + RelationGetDescr(pg_constraint), + &isnull); + if (isnull) + elog(ERROR, "null conbin for relation \"%s\"", + RelationGetRelationName(rel)); + else + { + char *exprString; + Node *expr; + Bitmapset *expr_attrs = NULL; + + exprString = TextDatumGetCString(exprDatum); + expr = stringToNode(exprString); + pfree(exprString); + + /* Find all attributes referenced */ + pull_varattnos(expr, 1, &expr_attrs); + + /* + * If the CHECK constraint contains whole-row reference then + * remember it. + */ + if (bms_is_member(InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber, expr_attrs)) + { + RememberConstraintForRebuilding(conform->oid, tab); + } + } + } + systable_endscan(conscan); + table_close(pg_constraint, AccessShareLock); + } + + /* + * Now check indexes with whole-row references. Prepare to scan pg_index + * for entries having indrelid matching this relation. + */ + ScanKeyInit(&skey, + Anum_pg_index_indrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(rel))); + + pg_index = table_open(IndexRelationId, AccessShareLock); + + indscan = systable_beginscan(pg_index, + IndexIndrelidIndexId, + true, + NULL, + 1, + &skey); + + while (HeapTupleIsValid(indexTuple = systable_getnext(indscan))) + { + Form_pg_index index = (Form_pg_index) GETSTRUCT(indexTuple); + Datum exprDatum; + + exprDatum = heap_getattr(indexTuple, + Anum_pg_index_indexprs, + RelationGetDescr(pg_index), + &isnull); + if (!isnull) + { + char *exprString; + Node *expr; + Bitmapset *expr_attrs = NULL; + + exprString = TextDatumGetCString(exprDatum); + expr = stringToNode(exprString); + pfree(exprString); + + /* Find all attributes referenced */ + pull_varattnos(expr, 1, &expr_attrs); + + /* + * If the index expression contains a whole-row reference then + * remember it. + */ + if (bms_is_member(InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber, expr_attrs)) + { + RememberIndexForRebuilding(index->indexrelid, tab); + continue; + } + } + + exprDatum = heap_getattr(indexTuple, + Anum_pg_index_indpred, + RelationGetDescr(pg_index), + &isnull); + if (!isnull) + { + char *exprString; + Node *expr; + Bitmapset *expr_attrs = NULL; + + exprString = TextDatumGetCString(exprDatum); + expr = stringToNode(exprString); + pfree(exprString); + + /* Find all attributes referenced */ + pull_varattnos(expr, 1, &expr_attrs); + + /* + * If the index predicate expression contains a whole-row + * reference then remember it. + */ + if (bms_is_member(InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber, expr_attrs)) + { + RememberIndexForRebuilding(index->indexrelid, tab); + } + } + } + + systable_endscan(indscan); + table_close(pg_index, AccessShareLock); +} + +/* + * Subroutine for ATExecAlterColumnType: remember that a replica identity + * needs to be reset. + */ +static void +RememberReplicaIdentityForRebuilding(Oid indoid, AlteredTableInfo *tab) +{ + if (!get_index_isreplident(indoid)) + return; + + if (tab->replicaIdentityIndex) + elog(ERROR, "relation %u has multiple indexes marked as replica identity", tab->relid); + + tab->replicaIdentityIndex = get_rel_name(indoid); +} + +/* + * Subroutine for ATExecAlterColumnType: remember any clustered index. + */ +static void +RememberClusterOnForRebuilding(Oid indoid, AlteredTableInfo *tab) +{ + if (!get_index_isclustered(indoid)) + return; + + if (tab->clusterOnIndex) + elog(ERROR, "relation %u has multiple clustered indexes", tab->relid); + + tab->clusterOnIndex = get_rel_name(indoid); +} + +/* + * Subroutine for ATExecAlterColumnType: remember that a constraint needs + * to be rebuilt (which we might already know). + */ +static void +RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab) +{ + /* + * This de-duplication check is critical for two independent reasons: we + * mustn't try to recreate the same constraint twice, and if a constraint + * depends on more than one column whose type is to be altered, we must + * capture its definition string before applying any of the column type + * changes. ruleutils.c will get confused if we ask again later. + */ + if (!list_member_oid(tab->changedConstraintOids, conoid)) + { + /* OK, capture the constraint's existing definition string */ + char *defstring = pg_get_constraintdef_command(conoid); + Oid indoid; + + /* + * It is critical to create not-null constraints ahead of primary key + * indexes; otherwise, the not-null constraint would be created by the + * primary key, and the constraint name would be wrong. + */ + if (get_constraint_type(conoid) == CONSTRAINT_NOTNULL) + { + tab->changedConstraintOids = lcons_oid(conoid, + tab->changedConstraintOids); + tab->changedConstraintDefs = lcons(defstring, + tab->changedConstraintDefs); + } + else + { + + tab->changedConstraintOids = lappend_oid(tab->changedConstraintOids, + conoid); + tab->changedConstraintDefs = lappend(tab->changedConstraintDefs, + defstring); + } + + /* + * For the index of a constraint, if any, remember if it is used for + * the table's replica identity or if it is a clustered index, so that + * ATPostAlterTypeCleanup() can queue up commands necessary to restore + * those properties. + */ + indoid = get_constraint_index(conoid); + if (OidIsValid(indoid)) + { + RememberReplicaIdentityForRebuilding(indoid, tab); + RememberClusterOnForRebuilding(indoid, tab); + } + } +} + +/* + * Subroutine for ATExecAlterColumnType: remember that an index needs + * to be rebuilt (which we might already know). + */ +static void +RememberIndexForRebuilding(Oid indoid, AlteredTableInfo *tab) +{ + /* + * This de-duplication check is critical for two independent reasons: we + * mustn't try to recreate the same index twice, and if an index depends + * on more than one column whose type is to be altered, we must capture + * its definition string before applying any of the column type changes. + * ruleutils.c will get confused if we ask again later. + */ + if (!list_member_oid(tab->changedIndexOids, indoid)) + { + /* + * Before adding it as an index-to-rebuild, we'd better see if it + * belongs to a constraint, and if so rebuild the constraint instead. + * Typically this check fails, because constraint indexes normally + * have only dependencies on their constraint. But it's possible for + * such an index to also have direct dependencies on table columns, + * for example with a partial exclusion constraint. + */ + Oid conoid = get_index_constraint(indoid); + + if (OidIsValid(conoid)) + { + RememberConstraintForRebuilding(conoid, tab); + } + else + { + /* OK, capture the index's existing definition string */ + char *defstring = pg_get_indexdef_string(indoid); + + tab->changedIndexOids = lappend_oid(tab->changedIndexOids, + indoid); + tab->changedIndexDefs = lappend(tab->changedIndexDefs, + defstring); + + /* + * Remember if this index is used for the table's replica identity + * or if it is a clustered index, so that ATPostAlterTypeCleanup() + * can queue up commands necessary to restore those properties. + */ + RememberReplicaIdentityForRebuilding(indoid, tab); + RememberClusterOnForRebuilding(indoid, tab); + } + } +} + +/* + * Subroutine for ATExecAlterColumnType: remember that a statistics object + * needs to be rebuilt (which we might already know). + */ +static void +RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab) +{ + /* + * This de-duplication check is critical for two independent reasons: we + * mustn't try to recreate the same statistics object twice, and if the + * statistics object depends on more than one column whose type is to be + * altered, we must capture its definition string before applying any of + * the type changes. ruleutils.c will get confused if we ask again later. + */ + if (!list_member_oid(tab->changedStatisticsOids, stxoid)) + { + /* OK, capture the statistics object's existing definition string */ + char *defstring = pg_get_statisticsobjdef_string(stxoid); + + tab->changedStatisticsOids = lappend_oid(tab->changedStatisticsOids, + stxoid); + tab->changedStatisticsDefs = lappend(tab->changedStatisticsDefs, + defstring); + } +} + +/* + * Cleanup after we've finished all the ALTER TYPE or SET EXPRESSION + * operations for a particular relation. We have to drop and recreate all the + * indexes and constraints that depend on the altered columns. We do the + * actual dropping here, but re-creation is managed by adding work queue + * entries to do those steps later. + */ +static void +ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) +{ + ObjectAddress obj; + ObjectAddresses *objects; + ListCell *def_item; + ListCell *oid_item; + + /* + * Collect all the constraints and indexes to drop so we can process them + * in a single call. That way we don't have to worry about dependencies + * among them. + */ + objects = new_object_addresses(); + + /* + * 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 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 + * resolve differently. Instead, we've got to use the OID of the + * constraint or index we're processing to figure out which relation to + * operate on. + */ + forboth(oid_item, tab->changedConstraintOids, + def_item, tab->changedConstraintDefs) + { + Oid oldId = lfirst_oid(oid_item); + HeapTuple tup; + Form_pg_constraint con; + Oid relid; + Oid confrelid; + bool conislocal; + + tup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(oldId)); + if (!HeapTupleIsValid(tup)) /* should not happen */ + elog(ERROR, "cache lookup failed for constraint %u", oldId); + con = (Form_pg_constraint) GETSTRUCT(tup); + if (OidIsValid(con->conrelid)) + relid = con->conrelid; + else + { + /* must be a domain constraint */ + relid = get_typ_typrelid(getBaseType(con->contypid)); + if (!OidIsValid(relid)) + elog(ERROR, "could not identify relation associated with constraint %u", oldId); + } + confrelid = con->confrelid; + conislocal = con->conislocal; + ReleaseSysCache(tup); + + ObjectAddressSet(obj, ConstraintRelationId, oldId); + add_exact_object_address(&obj, objects); + + /* + * If the constraint is inherited (only), we don't want to inject a + * new definition here; it'll get recreated when + * ATAddCheckNNConstraint recurses from adding the parent table's + * constraint. But we had to carry the info this far so that we can + * drop the constraint below. + */ + if (!conislocal) + continue; + + /* + * 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) + LockRelationOid(relid, AccessExclusiveLock); + + ATPostAlterTypeParse(oldId, relid, confrelid, + (char *) lfirst(def_item), + wqueue, lockmode, tab->rewrite); + } + forboth(oid_item, tab->changedIndexOids, + def_item, tab->changedIndexDefs) + { + Oid oldId = lfirst_oid(oid_item); + 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); + + ObjectAddressSet(obj, RelationRelationId, oldId); + add_exact_object_address(&obj, objects); + } + + /* add dependencies for new statistics */ + forboth(oid_item, tab->changedStatisticsOids, + def_item, tab->changedStatisticsDefs) + { + Oid oldId = lfirst_oid(oid_item); + 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); + + ObjectAddressSet(obj, StatisticExtRelationId, oldId); + add_exact_object_address(&obj, objects); + } + + /* + * Queue up command to restore replica identity index marking + */ + if (tab->replicaIdentityIndex) + { + AlterTableCmd *cmd = makeNode(AlterTableCmd); + ReplicaIdentityStmt *subcmd = makeNode(ReplicaIdentityStmt); + + subcmd->identity_type = REPLICA_IDENTITY_INDEX; + subcmd->name = tab->replicaIdentityIndex; + cmd->subtype = AT_ReplicaIdentity; + cmd->def = (Node *) subcmd; + + /* do it after indexes and constraints */ + tab->subcmds[AT_PASS_OLD_CONSTR] = + lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd); + } + + /* + * Queue up command to restore marking of index used for cluster. + */ + if (tab->clusterOnIndex) + { + AlterTableCmd *cmd = makeNode(AlterTableCmd); + + cmd->subtype = AT_ClusterOn; + cmd->name = tab->clusterOnIndex; + + /* do it after indexes and constraints */ + tab->subcmds[AT_PASS_OLD_CONSTR] = + lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd); + } + + /* + * It should be okay to use DROP_RESTRICT here, since nothing else should + * be depending on these objects. + */ + performMultipleDeletions(objects, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); + + free_object_addresses(objects); + + /* + * The objects will get recreated during subsequent passes over the work + * queue. + */ +} + +/* + * Parse the previously-saved definition string for a constraint, index or + * statistics object against the newly-established column data type(s), and + * queue up the resulting command parsetrees for execution. + * + * This might fail if, for example, you have a WHERE clause that uses an + * operator that's not available for the new column type. + */ +static void +ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd, + List **wqueue, LOCKMODE lockmode, bool rewrite) +{ + List *raw_parsetree_list; + List *querytree_list; + ListCell *list_item; + Relation rel; + + /* + * We expect that we will get only ALTER TABLE and CREATE INDEX + * statements. Hence, there is no need to pass them through + * parse_analyze_*() or the rewriter, but instead we need to pass them + * through parse_utilcmd.c to make them ready for execution. + */ + raw_parsetree_list = raw_parser(cmd, RAW_PARSE_DEFAULT); + querytree_list = NIL; + foreach(list_item, raw_parsetree_list) + { + RawStmt *rs = lfirst_node(RawStmt, list_item); + Node *stmt = rs->stmt; + + if (IsA(stmt, IndexStmt)) + querytree_list = lappend(querytree_list, + transformIndexStmt(oldRelId, + (IndexStmt *) stmt, + cmd)); + else if (IsA(stmt, AlterTableStmt)) + { + List *beforeStmts; + List *afterStmts; + + stmt = (Node *) transformAlterTableStmt(oldRelId, + (AlterTableStmt *) stmt, + cmd, + &beforeStmts, + &afterStmts); + querytree_list = list_concat(querytree_list, beforeStmts); + querytree_list = lappend(querytree_list, stmt); + querytree_list = list_concat(querytree_list, afterStmts); + } + else if (IsA(stmt, CreateStatsStmt)) + querytree_list = lappend(querytree_list, + transformStatsStmt(oldRelId, + (CreateStatsStmt *) stmt, + cmd)); + else + querytree_list = lappend(querytree_list, stmt); + } + + /* Caller should already have acquired whatever lock we need. */ + rel = relation_open(oldRelId, NoLock); + + /* + * Attach each generated command to the proper place in the work queue. + * Note this could result in creation of entirely new work-queue entries. + * + * Also note that we have to tweak the command subtypes, because it turns + * out that re-creation of indexes and constraints has to act a bit + * differently from initial creation. + */ + foreach(list_item, querytree_list) + { + Node *stm = (Node *) lfirst(list_item); + AlteredTableInfo *tab; + + tab = ATGetQueueEntry(wqueue, rel); + + if (IsA(stm, IndexStmt)) + { + IndexStmt *stmt = (IndexStmt *) stm; + AlterTableCmd *newcmd; + + if (!rewrite) + TryReuseIndex(oldId, stmt); + stmt->reset_default_tblspc = true; + /* keep the index's comment */ + stmt->idxcomment = GetComment(oldId, RelationRelationId, 0); + + newcmd = makeNode(AlterTableCmd); + newcmd->subtype = AT_ReAddIndex; + newcmd->def = (Node *) stmt; + tab->subcmds[AT_PASS_OLD_INDEX] = + lappend(tab->subcmds[AT_PASS_OLD_INDEX], newcmd); + } + else if (IsA(stm, AlterTableStmt)) + { + AlterTableStmt *stmt = (AlterTableStmt *) stm; + ListCell *lcmd; + + foreach(lcmd, stmt->cmds) + { + AlterTableCmd *cmd = lfirst_node(AlterTableCmd, lcmd); + + if (cmd->subtype == AT_AddIndex) + { + IndexStmt *indstmt; + Oid indoid; + + indstmt = castNode(IndexStmt, cmd->def); + indoid = get_constraint_index(oldId); + + if (!rewrite) + TryReuseIndex(indoid, indstmt); + /* keep any comment on the index */ + indstmt->idxcomment = GetComment(indoid, + RelationRelationId, 0); + indstmt->reset_default_tblspc = true; + + cmd->subtype = AT_ReAddIndex; + tab->subcmds[AT_PASS_OLD_INDEX] = + lappend(tab->subcmds[AT_PASS_OLD_INDEX], cmd); + + /* recreate any comment on the constraint */ + RebuildConstraintComment(tab, + AT_PASS_OLD_INDEX, + oldId, + rel, + NIL, + indstmt->idxname); + } + else if (cmd->subtype == AT_AddConstraint) + { + Constraint *con = castNode(Constraint, cmd->def); + + con->old_pktable_oid = refRelId; + /* rewriting neither side of a FK */ + if (con->contype == CONSTR_FOREIGN && + !rewrite && tab->rewrite == 0) + TryReuseForeignKey(oldId, con); + con->reset_default_tblspc = true; + cmd->subtype = AT_ReAddConstraint; + tab->subcmds[AT_PASS_OLD_CONSTR] = + lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd); + + /* + * Recreate any comment on the constraint. If we have + * recreated a primary key, then transformTableConstraint + * has added an unnamed not-null constraint here; skip + * this in that case. + */ + if (con->conname) + RebuildConstraintComment(tab, + AT_PASS_OLD_CONSTR, + oldId, + rel, + NIL, + con->conname); + else + Assert(con->contype == CONSTR_NOTNULL); + } + else + elog(ERROR, "unexpected statement subtype: %d", + (int) cmd->subtype); + } + } + else if (IsA(stm, AlterDomainStmt)) + { + AlterDomainStmt *stmt = (AlterDomainStmt *) stm; + + if (stmt->subtype == AD_AddConstraint) + { + Constraint *con = castNode(Constraint, stmt->def); + AlterTableCmd *cmd = makeNode(AlterTableCmd); + + cmd->subtype = AT_ReAddDomainConstraint; + cmd->def = (Node *) stmt; + tab->subcmds[AT_PASS_OLD_CONSTR] = + lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd); + + /* recreate any comment on the constraint */ + RebuildConstraintComment(tab, + AT_PASS_OLD_CONSTR, + oldId, + NULL, + stmt->typeName, + con->conname); + } + else + elog(ERROR, "unexpected statement subtype: %d", + (int) stmt->subtype); + } + else if (IsA(stm, CreateStatsStmt)) + { + CreateStatsStmt *stmt = (CreateStatsStmt *) stm; + AlterTableCmd *newcmd; + + /* keep the statistics object's comment */ + stmt->stxcomment = GetComment(oldId, StatisticExtRelationId, 0); + + newcmd = makeNode(AlterTableCmd); + newcmd->subtype = AT_ReAddStatistics; + newcmd->def = (Node *) stmt; + tab->subcmds[AT_PASS_MISC] = + lappend(tab->subcmds[AT_PASS_MISC], newcmd); + } + else + elog(ERROR, "unexpected statement type: %d", + (int) nodeTag(stm)); + } + + relation_close(rel, NoLock); +} + +/* + * Subroutine for ATPostAlterTypeParse() to recreate any existing comment + * for a table or domain constraint that is being rebuilt. + * + * objid is the OID of the constraint. + * Pass "rel" for a table constraint, or "domname" (domain's qualified name + * as a string list) for a domain constraint. + * (We could dig that info, as well as the conname, out of the pg_constraint + * entry; but callers already have them so might as well pass them.) + */ +static void +RebuildConstraintComment(AlteredTableInfo *tab, AlterTablePass pass, Oid objid, + Relation rel, List *domname, + const char *conname) +{ + CommentStmt *cmd; + char *comment_str; + AlterTableCmd *newcmd; + + /* Look for comment for object wanted, and leave if none */ + comment_str = GetComment(objid, ConstraintRelationId, 0); + if (comment_str == NULL) + return; + + /* Build CommentStmt node, copying all input data for safety */ + cmd = makeNode(CommentStmt); + if (rel) + { + cmd->objtype = OBJECT_TABCONSTRAINT; + cmd->object = (Node *) + list_make3(makeString(get_namespace_name(RelationGetNamespace(rel))), + makeString(pstrdup(RelationGetRelationName(rel))), + makeString(pstrdup(conname))); + } + else + { + cmd->objtype = OBJECT_DOMCONSTRAINT; + cmd->object = (Node *) + list_make2(makeTypeNameFromNameList(copyObject(domname)), + makeString(pstrdup(conname))); + } + cmd->comment = comment_str; + + /* Append it to list of commands */ + newcmd = makeNode(AlterTableCmd); + newcmd->subtype = AT_ReAddComment; + newcmd->def = (Node *) cmd; + tab->subcmds[pass] = lappend(tab->subcmds[pass], newcmd); +} + +/* + * 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) +{ + if (CheckIndexCompatible(oldId, + stmt->accessMethod, + stmt->indexParams, + stmt->excludeOpNames, + stmt->iswithoutoverlaps)) + { + Relation irel = index_open(oldId, NoLock); + + /* If it's a partitioned index, there is no storage to share. */ + if (irel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX) + { + stmt->oldNumber = irel->rd_locator.relNumber; + stmt->oldCreateSubid = irel->rd_createSubid; + stmt->oldFirstRelfilelocatorSubid = irel->rd_firstRelfilelocatorSubid; + } + index_close(irel, NoLock); + } +} + +/* + * 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; + 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 = SysCacheGetAttrNotNull(CONSTROID, tup, + Anum_pg_constraint_conpfeqop); + 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 = lappend_oid(con->old_conpfeqop, rawarr[i]); + + ReleaseSysCache(tup); +} + +/* + * ALTER COLUMN .. OPTIONS ( ... ) + * + * Returns the address of the modified column + */ +static ObjectAddress +ATExecAlterColumnGenericOptions(Relation rel, + const char *colName, + List *options, + LOCKMODE lockmode) +{ + Relation ftrel; + Relation attrel; + ForeignServer *server; + ForeignDataWrapper *fdw; + HeapTuple tuple; + HeapTuple newtuple; + bool isnull; + Datum repl_val[Natts_pg_attribute]; + bool repl_null[Natts_pg_attribute]; + bool repl_repl[Natts_pg_attribute]; + Datum datum; + Form_pg_foreign_table fttableform; + Form_pg_attribute atttableform; + AttrNumber attnum; + ObjectAddress address; + + if (options == NIL) + return InvalidObjectAddress; + + /* First, determine FDW validator associated to the foreign table. */ + ftrel = table_open(ForeignTableRelationId, AccessShareLock); + tuple = SearchSysCache1(FOREIGNTABLEREL, ObjectIdGetDatum(rel->rd_id)); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("foreign table \"%s\" does not exist", + RelationGetRelationName(rel)))); + fttableform = (Form_pg_foreign_table) GETSTRUCT(tuple); + server = GetForeignServer(fttableform->ftserver); + fdw = GetForeignDataWrapper(server->fdwid); + + table_close(ftrel, AccessShareLock); + ReleaseSysCache(tuple); + + attrel = table_open(AttributeRelationId, RowExclusiveLock); + tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + colName, RelationGetRelationName(rel)))); + + /* Prevent them from altering a system attribute */ + atttableform = (Form_pg_attribute) GETSTRUCT(tuple); + attnum = atttableform->attnum; + if (attnum <= 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter system column \"%s\"", colName))); + + + /* Initialize buffers for new tuple values */ + memset(repl_val, 0, sizeof(repl_val)); + memset(repl_null, false, sizeof(repl_null)); + memset(repl_repl, false, sizeof(repl_repl)); + + /* Extract the current options */ + datum = SysCacheGetAttr(ATTNAME, + tuple, + Anum_pg_attribute_attfdwoptions, + &isnull); + if (isnull) + datum = PointerGetDatum(NULL); + + /* Transform the options */ + datum = transformGenericOptions(AttributeRelationId, + datum, + options, + fdw->fdwvalidator); + + if (DatumGetPointer(datum) != NULL) + repl_val[Anum_pg_attribute_attfdwoptions - 1] = datum; + else + repl_null[Anum_pg_attribute_attfdwoptions - 1] = true; + + repl_repl[Anum_pg_attribute_attfdwoptions - 1] = true; + + /* Everything looks good - update the tuple */ + + newtuple = heap_modify_tuple(tuple, RelationGetDescr(attrel), + repl_val, repl_null, repl_repl); + + CatalogTupleUpdate(attrel, &newtuple->t_self, newtuple); + + InvokeObjectPostAlterHook(RelationRelationId, + RelationGetRelid(rel), + atttableform->attnum); + ObjectAddressSubSet(address, RelationRelationId, + RelationGetRelid(rel), attnum); + + ReleaseSysCache(tuple); + + table_close(attrel, RowExclusiveLock); + + heap_freetuple(newtuple); + + return address; +} + +/* + * ALTER TABLE OWNER + * + * recursing is true if we are recursing from a table to its indexes, + * sequences, or toast table. We don't allow the ownership of those things to + * be changed separately from the parent table. Also, we can skip permission + * checks (this is necessary not just an optimization, else we'd fail to + * handle toast tables properly). + * + * recursing is also true if ALTER TYPE OWNER is calling us to fix up a + * free-standing composite type. + */ +void +ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lockmode) +{ + Relation target_rel; + Relation class_rel; + HeapTuple tuple; + Form_pg_class tuple_class; + + /* + * Get exclusive lock till end of transaction on the target table. Use + * relation_open so that we can work on indexes and sequences. + */ + target_rel = relation_open(relationOid, lockmode); + + /* Get its pg_class tuple, too */ + class_rel = table_open(RelationRelationId, RowExclusiveLock); + + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relationOid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for relation %u", relationOid); + tuple_class = (Form_pg_class) GETSTRUCT(tuple); + + /* Can we change the ownership of this tuple? */ + switch (tuple_class->relkind) + { + case RELKIND_RELATION: + case RELKIND_VIEW: + case RELKIND_MATVIEW: + case RELKIND_FOREIGN_TABLE: + case RELKIND_PARTITIONED_TABLE: + case RELKIND_PROPGRAPH: + /* ok to change owner */ + break; + case RELKIND_INDEX: + if (!recursing) + { + /* + * Because ALTER INDEX OWNER used to be allowed, and in fact + * is generated by old versions of pg_dump, we give a warning + * and do nothing rather than erroring out. Also, to avoid + * unnecessary chatter while restoring those old dumps, say + * nothing at all if the command would be a no-op anyway. + */ + if (tuple_class->relowner != newOwnerId) + ereport(WARNING, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot change owner of index \"%s\"", + NameStr(tuple_class->relname)), + errhint("Change the ownership of the index's table instead."))); + /* quick hack to exit via the no-op path */ + newOwnerId = tuple_class->relowner; + } + break; + case RELKIND_PARTITIONED_INDEX: + if (recursing) + break; + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot change owner of index \"%s\"", + NameStr(tuple_class->relname)), + errhint("Change the ownership of the index's table instead."))); + break; + case RELKIND_SEQUENCE: + if (!recursing && + tuple_class->relowner != newOwnerId) + { + /* if it's an owned sequence, disallow changing it by itself */ + Oid tableId; + int32 colId; + + if (sequenceIsOwned(relationOid, DEPENDENCY_AUTO, &tableId, &colId) || + sequenceIsOwned(relationOid, DEPENDENCY_INTERNAL, &tableId, &colId)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot change owner of sequence \"%s\"", + NameStr(tuple_class->relname)), + errdetail("Sequence \"%s\" is linked to table \"%s\".", + NameStr(tuple_class->relname), + get_rel_name(tableId)))); + } + break; + case RELKIND_COMPOSITE_TYPE: + if (recursing) + break; + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is a composite type", + NameStr(tuple_class->relname)), + /* translator: %s is an SQL ALTER command */ + errhint("Use %s instead.", + "ALTER TYPE"))); + break; + case RELKIND_TOASTVALUE: + if (recursing) + break; + pg_fallthrough; + default: + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot change owner of relation \"%s\"", + NameStr(tuple_class->relname)), + errdetail_relkind_not_supported(tuple_class->relkind))); + } + + /* + * If the new owner is the same as the existing owner, consider the + * command to have succeeded. This is for dump restoration purposes. + */ + if (tuple_class->relowner != newOwnerId) + { + Datum repl_val[Natts_pg_class]; + bool repl_null[Natts_pg_class]; + bool repl_repl[Natts_pg_class]; + Acl *newAcl; + Datum aclDatum; + bool isNull; + HeapTuple newtuple; + + /* skip permission checks when recursing to index or toast table */ + if (!recursing) + { + /* Superusers can always do it */ + if (!superuser()) + { + Oid namespaceOid = tuple_class->relnamespace; + AclResult aclresult; + + /* Otherwise, must be owner of the existing object */ + if (!object_ownercheck(RelationRelationId, relationOid, GetUserId())) + aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relationOid)), + RelationGetRelationName(target_rel)); + + /* Must be able to become new owner */ + check_can_set_role(GetUserId(), newOwnerId); + + /* New owner must have CREATE privilege on namespace */ + aclresult = object_aclcheck(NamespaceRelationId, namespaceOid, newOwnerId, + ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_SCHEMA, + get_namespace_name(namespaceOid)); + } + } + + memset(repl_null, false, sizeof(repl_null)); + memset(repl_repl, false, sizeof(repl_repl)); + + repl_repl[Anum_pg_class_relowner - 1] = true; + repl_val[Anum_pg_class_relowner - 1] = ObjectIdGetDatum(newOwnerId); + + /* + * Determine the modified ACL for the new owner. This is only + * necessary when the ACL is non-null. + */ + aclDatum = SysCacheGetAttr(RELOID, tuple, + Anum_pg_class_relacl, + &isNull); + if (!isNull) + { + newAcl = aclnewowner(DatumGetAclP(aclDatum), + tuple_class->relowner, newOwnerId); + repl_repl[Anum_pg_class_relacl - 1] = true; + repl_val[Anum_pg_class_relacl - 1] = PointerGetDatum(newAcl); + } + + newtuple = heap_modify_tuple(tuple, RelationGetDescr(class_rel), repl_val, repl_null, repl_repl); + + CatalogTupleUpdate(class_rel, &newtuple->t_self, newtuple); + + heap_freetuple(newtuple); + + /* + * We must similarly update any per-column ACLs to reflect the new + * owner; for neatness reasons that's split out as a subroutine. + */ + change_owner_fix_column_acls(relationOid, + tuple_class->relowner, + newOwnerId); + + /* + * Update owner dependency reference, if any. A composite type has + * none, because it's tracked for the pg_type entry instead of here; + * indexes and TOAST tables don't have their own entries either. + */ + if (tuple_class->relkind != RELKIND_COMPOSITE_TYPE && + tuple_class->relkind != RELKIND_INDEX && + tuple_class->relkind != RELKIND_PARTITIONED_INDEX && + tuple_class->relkind != RELKIND_TOASTVALUE) + changeDependencyOnOwner(RelationRelationId, relationOid, + newOwnerId); + + /* + * Also change the ownership of the table's row type, if it has one + */ + if (OidIsValid(tuple_class->reltype)) + AlterTypeOwnerInternal(tuple_class->reltype, newOwnerId); + + /* + * If we are operating on a table or materialized view, also change + * the ownership of any indexes and sequences that belong to the + * relation, as well as its toast table (if it has one). + */ + if (tuple_class->relkind == RELKIND_RELATION || + tuple_class->relkind == RELKIND_PARTITIONED_TABLE || + tuple_class->relkind == RELKIND_MATVIEW || + tuple_class->relkind == RELKIND_TOASTVALUE) + { + List *index_oid_list; + ListCell *i; + + /* Find all the indexes belonging to this relation */ + index_oid_list = RelationGetIndexList(target_rel); + + /* For each index, recursively change its ownership */ + foreach(i, index_oid_list) + ATExecChangeOwner(lfirst_oid(i), newOwnerId, true, lockmode); + + list_free(index_oid_list); + } + + /* If it has a toast table, recurse to change its ownership */ + if (tuple_class->reltoastrelid != InvalidOid) + ATExecChangeOwner(tuple_class->reltoastrelid, newOwnerId, + true, lockmode); + + /* If it has dependent sequences, recurse to change them too */ + change_owner_recurse_to_sequences(relationOid, newOwnerId, lockmode); + } + + InvokeObjectPostAlterHook(RelationRelationId, relationOid, 0); + + ReleaseSysCache(tuple); + table_close(class_rel, RowExclusiveLock); + relation_close(target_rel, NoLock); +} + +/* + * change_owner_fix_column_acls + * + * Helper function for ATExecChangeOwner. Scan the columns of the table + * and fix any non-null column ACLs to reflect the new owner. + */ +static void +change_owner_fix_column_acls(Oid relationOid, Oid oldOwnerId, Oid newOwnerId) +{ + Relation attRelation; + SysScanDesc scan; + ScanKeyData key[1]; + HeapTuple attributeTuple; + + attRelation = table_open(AttributeRelationId, RowExclusiveLock); + ScanKeyInit(&key[0], + Anum_pg_attribute_attrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relationOid)); + scan = systable_beginscan(attRelation, AttributeRelidNumIndexId, + true, NULL, 1, key); + while (HeapTupleIsValid(attributeTuple = systable_getnext(scan))) + { + Form_pg_attribute att = (Form_pg_attribute) GETSTRUCT(attributeTuple); + Datum repl_val[Natts_pg_attribute]; + bool repl_null[Natts_pg_attribute]; + bool repl_repl[Natts_pg_attribute]; + Acl *newAcl; + Datum aclDatum; + bool isNull; + HeapTuple newtuple; + + /* Ignore dropped columns */ + if (att->attisdropped) + continue; + + aclDatum = heap_getattr(attributeTuple, + Anum_pg_attribute_attacl, + RelationGetDescr(attRelation), + &isNull); + /* Null ACLs do not require changes */ + if (isNull) + continue; + + memset(repl_null, false, sizeof(repl_null)); + memset(repl_repl, false, sizeof(repl_repl)); + + newAcl = aclnewowner(DatumGetAclP(aclDatum), + oldOwnerId, newOwnerId); + repl_repl[Anum_pg_attribute_attacl - 1] = true; + repl_val[Anum_pg_attribute_attacl - 1] = PointerGetDatum(newAcl); + + newtuple = heap_modify_tuple(attributeTuple, + RelationGetDescr(attRelation), + repl_val, repl_null, repl_repl); + + CatalogTupleUpdate(attRelation, &newtuple->t_self, newtuple); + + heap_freetuple(newtuple); + } + systable_endscan(scan); + table_close(attRelation, RowExclusiveLock); +} + +/* + * change_owner_recurse_to_sequences + * + * Helper function for ATExecChangeOwner. Examines pg_depend searching + * for sequences that are dependent on serial columns, and changes their + * ownership. + */ +static void +change_owner_recurse_to_sequences(Oid relationOid, Oid newOwnerId, LOCKMODE lockmode) +{ + Relation depRel; + SysScanDesc scan; + ScanKeyData key[2]; + HeapTuple tup; + + /* + * SERIAL sequences are those having an auto dependency on one of the + * table's columns (we don't care *which* column, exactly). + */ + depRel = table_open(DependRelationId, AccessShareLock); + + ScanKeyInit(&key[0], + Anum_pg_depend_refclassid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationRelationId)); + ScanKeyInit(&key[1], + Anum_pg_depend_refobjid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relationOid)); + /* we leave refobjsubid unspecified */ + + scan = systable_beginscan(depRel, DependReferenceIndexId, true, + NULL, 2, key); + + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_depend depForm = (Form_pg_depend) GETSTRUCT(tup); + Relation seqRel; + + /* skip dependencies other than auto dependencies on columns */ + if (depForm->refobjsubid == 0 || + depForm->classid != RelationRelationId || + depForm->objsubid != 0 || + !(depForm->deptype == DEPENDENCY_AUTO || depForm->deptype == DEPENDENCY_INTERNAL)) + continue; + + /* Use relation_open just in case it's an index */ + seqRel = relation_open(depForm->objid, lockmode); + + /* skip non-sequence relations */ + if (RelationGetForm(seqRel)->relkind != RELKIND_SEQUENCE) + { + /* No need to keep the lock */ + relation_close(seqRel, lockmode); + continue; + } + + /* We don't need to close the sequence while we alter it. */ + ATExecChangeOwner(depForm->objid, newOwnerId, true, lockmode); + + /* Now we can close it. Keep the lock till end of transaction. */ + relation_close(seqRel, NoLock); + } + + systable_endscan(scan); + + relation_close(depRel, AccessShareLock); +} + +/* + * ALTER TABLE CLUSTER ON + * + * The only thing we have to do is to change the indisclustered bits. + * + * Return the address of the new clustering index. + */ +static ObjectAddress +ATExecClusterOn(Relation rel, const char *indexName, LOCKMODE lockmode) +{ + Oid indexOid; + ObjectAddress address; + + indexOid = get_relname_relid(indexName, rel->rd_rel->relnamespace); + + if (!OidIsValid(indexOid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("index \"%s\" for table \"%s\" does not exist", + indexName, RelationGetRelationName(rel)))); + + /* Check index is valid to cluster on */ + check_index_is_clusterable(rel, indexOid, lockmode); + + /* And do the work */ + mark_index_clustered(rel, indexOid, false); + + ObjectAddressSet(address, + RelationRelationId, indexOid); + + return address; +} + +/* + * ALTER TABLE SET WITHOUT CLUSTER + * + * We have to find any indexes on the table that have indisclustered bit + * set and turn it off. + */ +static void +ATExecDropCluster(Relation rel, LOCKMODE lockmode) +{ + mark_index_clustered(rel, InvalidOid, false); +} + +/* + * Preparation phase for SET ACCESS METHOD + * + * Check that the access method exists and determine whether a change is + * actually needed. + */ +static void +ATPrepSetAccessMethod(AlteredTableInfo *tab, Relation rel, const char *amname) +{ + Oid amoid; + + /* + * Look up the access method name and check that it differs from the + * table's current AM. If DEFAULT was specified for a partitioned table + * (amname is NULL), set it to InvalidOid to reset the catalogued AM. + */ + if (amname != NULL) + amoid = get_table_am_oid(amname, false); + else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + amoid = InvalidOid; + else + amoid = get_table_am_oid(default_table_access_method, false); + + /* if it's a match, phase 3 doesn't need to do anything */ + if (rel->rd_rel->relam == amoid) + return; + + /* Save info for Phase 3 to do the real work */ + tab->rewrite |= AT_REWRITE_ACCESS_METHOD; + tab->newAccessMethod = amoid; + tab->chgAccessMethod = true; +} + +/* + * Special handling of ALTER TABLE SET ACCESS METHOD for relations with no + * storage that have an interest in preserving AM. + * + * Since these have no storage, setting the access method is a catalog only + * operation. + */ +static void +ATExecSetAccessMethodNoStorage(Relation rel, Oid newAccessMethodId) +{ + Relation pg_class; + Oid oldAccessMethodId; + HeapTuple tuple; + Form_pg_class rd_rel; + Oid reloid = RelationGetRelid(rel); + + /* + * Shouldn't be called on relations having storage; these are processed in + * phase 3. + */ + Assert(!RELKIND_HAS_STORAGE(rel->rd_rel->relkind)); + + /* Get a modifiable copy of the relation's pg_class row. */ + pg_class = table_open(RelationRelationId, RowExclusiveLock); + + tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for relation %u", reloid); + rd_rel = (Form_pg_class) GETSTRUCT(tuple); + + /* Update the pg_class row. */ + oldAccessMethodId = rd_rel->relam; + rd_rel->relam = newAccessMethodId; + + /* Leave if no update required */ + if (rd_rel->relam == oldAccessMethodId) + { + heap_freetuple(tuple); + table_close(pg_class, RowExclusiveLock); + return; + } + + CatalogTupleUpdate(pg_class, &tuple->t_self, tuple); + + /* + * Update the dependency on the new access method. No dependency is added + * if the new access method is InvalidOid (default case). Be very careful + * that this has to compare the previous value stored in pg_class with the + * new one. + */ + if (!OidIsValid(oldAccessMethodId) && OidIsValid(rd_rel->relam)) + { + ObjectAddress relobj, + referenced; + + /* + * New access method is defined and there was no dependency + * previously, so record a new one. + */ + ObjectAddressSet(relobj, RelationRelationId, reloid); + ObjectAddressSet(referenced, AccessMethodRelationId, rd_rel->relam); + recordDependencyOn(&relobj, &referenced, DEPENDENCY_NORMAL); + } + else if (OidIsValid(oldAccessMethodId) && + !OidIsValid(rd_rel->relam)) + { + /* + * There was an access method defined, and no new one, so just remove + * the existing dependency. + */ + deleteDependencyRecordsForClass(RelationRelationId, reloid, + AccessMethodRelationId, + DEPENDENCY_NORMAL); + } + else + { + Assert(OidIsValid(oldAccessMethodId) && + OidIsValid(rd_rel->relam)); + + /* Both are valid, so update the dependency */ + changeDependencyFor(RelationRelationId, reloid, + AccessMethodRelationId, + oldAccessMethodId, rd_rel->relam); + } + + /* make the relam and dependency changes visible */ + CommandCounterIncrement(); + + InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0); + + heap_freetuple(tuple); + table_close(pg_class, RowExclusiveLock); +} + +/* + * ALTER TABLE SET TABLESPACE + */ +static void +ATPrepSetTableSpace(AlteredTableInfo *tab, Relation rel, const char *tablespacename, LOCKMODE lockmode) +{ + Oid tablespaceId; + + /* Check that the tablespace exists */ + tablespaceId = get_tablespace_oid(tablespacename, false); + + /* Check permissions except when moving to database's default */ + if (OidIsValid(tablespaceId) && tablespaceId != MyDatabaseTableSpace) + { + AclResult aclresult; + + aclresult = object_aclcheck(TableSpaceRelationId, tablespaceId, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, tablespacename); + } + + /* Save info for Phase 3 to do the real work */ + if (OidIsValid(tab->newTableSpace)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot have multiple SET TABLESPACE subcommands"))); + + tab->newTableSpace = tablespaceId; +} + +/* + * Set, reset, or replace reloptions. + */ +static void +ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation, + LOCKMODE lockmode) +{ + Oid relid; + Relation pgclass; + HeapTuple tuple; + HeapTuple newtuple; + Datum datum; + Datum newOptions; + Datum repl_val[Natts_pg_class]; + bool repl_null[Natts_pg_class]; + bool repl_repl[Natts_pg_class]; + const char *const validnsps[] = HEAP_RELOPT_NAMESPACES; + + if (defList == NIL && operation != AT_ReplaceRelOptions) + return; /* nothing to do */ + + pgclass = table_open(RelationRelationId, RowExclusiveLock); + + /* Fetch heap tuple */ + relid = RelationGetRelid(rel); + tuple = SearchSysCacheLocked1(RELOID, ObjectIdGetDatum(relid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for relation %u", relid); + + if (operation == AT_ReplaceRelOptions) + { + /* + * If we're supposed to replace the reloptions list, we just pretend + * there were none before. + */ + datum = (Datum) 0; + } + else + { + bool isnull; + + /* Get the old reloptions */ + datum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, + &isnull); + if (isnull) + datum = (Datum) 0; + } + + /* Generate new proposed reloptions (text array) */ + newOptions = transformRelOptions(datum, defList, NULL, validnsps, false, + operation == AT_ResetRelOptions); + + /* Validate */ + switch (rel->rd_rel->relkind) + { + case RELKIND_RELATION: + case RELKIND_MATVIEW: + (void) heap_reloptions(rel->rd_rel->relkind, newOptions, true); + break; + case RELKIND_PARTITIONED_TABLE: + (void) partitioned_table_reloptions(newOptions, true); + break; + case RELKIND_VIEW: + (void) view_reloptions(newOptions, true); + break; + case RELKIND_INDEX: + case RELKIND_PARTITIONED_INDEX: + (void) index_reloptions(rel->rd_indam->amoptions, newOptions, true); + break; + case RELKIND_TOASTVALUE: + /* fall through to error -- shouldn't ever get here */ + default: + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot set options for relation \"%s\"", + RelationGetRelationName(rel)), + errdetail_relkind_not_supported(rel->rd_rel->relkind))); + break; + } + + /* Special-case validation of view options */ + if (rel->rd_rel->relkind == RELKIND_VIEW) + { + Query *view_query = get_view_query(rel); + List *view_options = untransformRelOptions(newOptions); + ListCell *cell; + bool check_option = false; + + foreach(cell, view_options) + { + DefElem *defel = (DefElem *) lfirst(cell); + + if (strcmp(defel->defname, "check_option") == 0) + check_option = true; + } + + /* + * If the check option is specified, look to see if the view is + * actually auto-updatable or not. + */ + if (check_option) + { + const char *view_updatable_error = + view_query_is_auto_updatable(view_query, true); + + if (view_updatable_error) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("WITH CHECK OPTION is supported only on automatically updatable views"), + errhint("%s", _(view_updatable_error)))); + } + } + + /* + * All we need do here is update the pg_class row; the new options will be + * propagated into relcaches during post-commit cache inval. + */ + memset(repl_val, 0, sizeof(repl_val)); + memset(repl_null, false, sizeof(repl_null)); + memset(repl_repl, false, sizeof(repl_repl)); + + if (newOptions != (Datum) 0) + repl_val[Anum_pg_class_reloptions - 1] = newOptions; + else + repl_null[Anum_pg_class_reloptions - 1] = true; + + repl_repl[Anum_pg_class_reloptions - 1] = true; + + newtuple = heap_modify_tuple(tuple, RelationGetDescr(pgclass), + repl_val, repl_null, repl_repl); + + CatalogTupleUpdate(pgclass, &newtuple->t_self, newtuple); + UnlockTuple(pgclass, &tuple->t_self, InplaceUpdateTupleLock); + + InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0); + + heap_freetuple(newtuple); + + ReleaseSysCache(tuple); + + /* repeat the whole exercise for the toast table, if there's one */ + if (OidIsValid(rel->rd_rel->reltoastrelid)) + { + Relation toastrel; + Oid toastid = rel->rd_rel->reltoastrelid; + + toastrel = table_open(toastid, lockmode); + + /* Fetch heap tuple */ + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(toastid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for relation %u", toastid); + + if (operation == AT_ReplaceRelOptions) + { + /* + * If we're supposed to replace the reloptions list, we just + * pretend there were none before. + */ + datum = (Datum) 0; + } + else + { + bool isnull; + + /* Get the old reloptions */ + datum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, + &isnull); + if (isnull) + datum = (Datum) 0; + } ++<<<<<<< ours + + newOptions = transformRelOptions(datum, defList, "toast", validnsps, + false, operation == AT_ResetRelOptions); + + (void) heap_reloptions(RELKIND_TOASTVALUE, newOptions, true); + + memset(repl_val, 0, sizeof(repl_val)); + memset(repl_null, false, sizeof(repl_null)); + memset(repl_repl, false, sizeof(repl_repl)); + + if (newOptions != (Datum) 0) + repl_val[Anum_pg_class_reloptions - 1] = newOptions; + else + repl_null[Anum_pg_class_reloptions - 1] = true; + + repl_repl[Anum_pg_class_reloptions - 1] = true; + + newtuple = heap_modify_tuple(tuple, RelationGetDescr(pgclass), + repl_val, repl_null, repl_repl); + + CatalogTupleUpdate(pgclass, &newtuple->t_self, newtuple); + + InvokeObjectPostAlterHookArg(RelationRelationId, + RelationGetRelid(toastrel), 0, + InvalidOid, true); + + heap_freetuple(newtuple); + + ReleaseSysCache(tuple); + + table_close(toastrel, NoLock); + } + + table_close(pgclass, RowExclusiveLock); +} + +/* + * Execute ALTER TABLE SET TABLESPACE for cases where there is no tuple + * rewriting to be done, so we just want to copy the data as fast as possible. + */ +static void +ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode) +{ + Relation rel; + Oid reltoastrelid; + RelFileNumber newrelfilenumber; + RelFileLocator newrlocator; + List *reltoastidxids = NIL; + ListCell *lc; + + /* + * Need lock here in case we are recursing to toast table or index + */ + rel = relation_open(tableOid, lockmode); + + /* Check first if relation can be moved to new tablespace */ + if (!CheckRelationTableSpaceMove(rel, newTableSpace)) + { + InvokeObjectPostAlterHook(RelationRelationId, + RelationGetRelid(rel), 0); + relation_close(rel, NoLock); + return; + } + + reltoastrelid = rel->rd_rel->reltoastrelid; + /* Fetch the list of indexes on toast relation if necessary */ + if (OidIsValid(reltoastrelid)) + { + Relation toastRel = relation_open(reltoastrelid, lockmode); + + reltoastidxids = RelationGetIndexList(toastRel); + relation_close(toastRel, lockmode); + } + + /* + * Relfilenumbers are not unique in databases across tablespaces, so we + * need to allocate a new one in the new tablespace. + */ + newrelfilenumber = GetNewRelFileNumber(newTableSpace, NULL, + rel->rd_rel->relpersistence); + + /* Open old and new relation */ + newrlocator = rel->rd_locator; + newrlocator.relNumber = newrelfilenumber; + newrlocator.spcOid = newTableSpace; + + /* hand off to AM to actually create new rel storage and copy the data */ + if (rel->rd_rel->relkind == RELKIND_INDEX) + { + index_copy_data(rel, newrlocator); + } + else + { + Assert(RELKIND_HAS_TABLE_AM(rel->rd_rel->relkind)); + table_relation_copy_data(rel, &newrlocator); + } + + /* + * Update the pg_class row. + * + * NB: This wouldn't work if ATExecSetTableSpace() were allowed to be + * executed on pg_class or its indexes (the above copy wouldn't contain + * the updated pg_class entry), but that's forbidden with + * CheckRelationTableSpaceMove(). + */ + SetRelationTableSpace(rel, newTableSpace, newrelfilenumber); + + InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0); + + RelationAssumeNewRelfilelocator(rel); + + relation_close(rel, NoLock); + + /* Make sure the reltablespace change is visible */ + CommandCounterIncrement(); + + /* Move associated toast relation and/or indexes, too */ + if (OidIsValid(reltoastrelid)) + ATExecSetTableSpace(reltoastrelid, newTableSpace, lockmode); + foreach(lc, reltoastidxids) + ATExecSetTableSpace(lfirst_oid(lc), newTableSpace, lockmode); + + /* Clean up */ + list_free(reltoastidxids); +} + +/* + * Special handling of ALTER TABLE SET TABLESPACE for relations with no + * storage that have an interest in preserving tablespace. + * + * Since these have no storage the tablespace can be updated with a simple + * metadata only operation to update the tablespace. + */ +static void +ATExecSetTableSpaceNoStorage(Relation rel, Oid newTableSpace) +{ + /* + * Shouldn't be called on relations having storage; these are processed in + * phase 3. + */ + Assert(!RELKIND_HAS_STORAGE(rel->rd_rel->relkind)); + + /* check if relation can be moved to its new tablespace */ + if (!CheckRelationTableSpaceMove(rel, newTableSpace)) + { + InvokeObjectPostAlterHook(RelationRelationId, + RelationGetRelid(rel), + 0); + return; + } + + /* Update can be done, so change reltablespace */ + SetRelationTableSpace(rel, newTableSpace, InvalidOid); + + InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0); + + /* Make sure the reltablespace change is visible */ + CommandCounterIncrement(); +} + +/* + * Alter Table ALL ... SET TABLESPACE + * + * Allows a user to move all objects of some type in a given tablespace in the + * current database to another tablespace. Objects can be chosen based on the + * owner of the object also, to allow users to move only their objects. + * The user must have CREATE rights on the new tablespace, as usual. The main + * permissions handling is done by the lower-level table move function. + * + * All to-be-moved objects are locked first. If NOWAIT is specified and the + * lock can't be acquired then we ereport(ERROR). + */ +Oid +AlterTableMoveAll(AlterTableMoveAllStmt *stmt) +{ + List *relations = NIL; + ListCell *l; + ScanKeyData key[1]; + Relation rel; + TableScanDesc scan; + HeapTuple tuple; + Oid orig_tablespaceoid; + Oid new_tablespaceoid; + List *role_oids = roleSpecsToIds(stmt->roles); + + /* Ensure we were not asked to move something we can't */ + if (stmt->objtype != OBJECT_TABLE && stmt->objtype != OBJECT_INDEX && + stmt->objtype != OBJECT_MATVIEW) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("only tables, indexes, and materialized views exist in tablespaces"))); + + /* Get the orig and new tablespace OIDs */ + orig_tablespaceoid = get_tablespace_oid(stmt->orig_tablespacename, false); + new_tablespaceoid = get_tablespace_oid(stmt->new_tablespacename, false); + + /* Can't move shared relations in to or out of pg_global */ + /* This is also checked by ATExecSetTableSpace, but nice to stop earlier */ + if (orig_tablespaceoid == GLOBALTABLESPACE_OID || + new_tablespaceoid == GLOBALTABLESPACE_OID) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot move relations in to or out of pg_global tablespace"))); + + /* + * Must have CREATE rights on the new tablespace, unless it is the + * database default tablespace (which all users implicitly have CREATE + * rights on). + */ + if (OidIsValid(new_tablespaceoid) && new_tablespaceoid != MyDatabaseTableSpace) + { + AclResult aclresult; + + aclresult = object_aclcheck(TableSpaceRelationId, new_tablespaceoid, GetUserId(), + ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(new_tablespaceoid)); + } + + /* + * Now that the checks are done, check if we should set either to + * InvalidOid because it is our database's default tablespace. + */ + if (orig_tablespaceoid == MyDatabaseTableSpace) + orig_tablespaceoid = InvalidOid; + + if (new_tablespaceoid == MyDatabaseTableSpace) + new_tablespaceoid = InvalidOid; + + /* no-op */ + if (orig_tablespaceoid == new_tablespaceoid) + return new_tablespaceoid; + + /* + * Walk the list of objects in the tablespace and move them. This will + * only find objects in our database, of course. + */ + ScanKeyInit(&key[0], + Anum_pg_class_reltablespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(orig_tablespaceoid)); + + rel = table_open(RelationRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 1, key); + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple); + Oid relOid = relForm->oid; + + /* + * Do not move objects in pg_catalog as part of this, if an admin + * really wishes to do so, they can issue the individual ALTER + * commands directly. + * + * Also, explicitly avoid any shared tables, temp tables, TOAST (TOAST + * will be moved with the main table). + * + * Conflict log tables are system-managed for logical replication and + * cannot be altered directly, so skip them as well; otherwise a + * single such table in the source tablespace would abort the whole + * bulk move. + */ + if (IsCatalogNamespace(relForm->relnamespace) || + relForm->relisshared || + isAnyTempNamespace(relForm->relnamespace) || + IsToastNamespace(relForm->relnamespace) || + IsConflictLogTableNamespace(relForm->relnamespace)) + continue; + + /* Only move the object type requested */ + if ((stmt->objtype == OBJECT_TABLE && + relForm->relkind != RELKIND_RELATION && + relForm->relkind != RELKIND_PARTITIONED_TABLE) || + (stmt->objtype == OBJECT_INDEX && + relForm->relkind != RELKIND_INDEX && + relForm->relkind != RELKIND_PARTITIONED_INDEX) || + (stmt->objtype == OBJECT_MATVIEW && + relForm->relkind != RELKIND_MATVIEW)) + continue; + + /* Check if we are only moving objects owned by certain roles */ + if (role_oids != NIL && !list_member_oid(role_oids, relForm->relowner)) + continue; + + /* + * Handle permissions-checking here since we are locking the tables + * and also to avoid doing a bunch of work only to fail part-way. Note + * that permissions will also be checked by AlterTableInternal(). + * + * Caller must be considered an owner on the table to move it. + */ + if (!object_ownercheck(RelationRelationId, relOid, GetUserId())) + aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relOid)), + NameStr(relForm->relname)); + + if (stmt->nowait && + !ConditionalLockRelationOid(relOid, AccessExclusiveLock)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_IN_USE), + errmsg("aborting because lock on relation \"%s.%s\" is not available", + get_namespace_name(relForm->relnamespace), + NameStr(relForm->relname)))); + else + LockRelationOid(relOid, AccessExclusiveLock); + + /* Add to our list of objects to move */ + relations = lappend_oid(relations, relOid); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + + if (relations == NIL) + ereport(NOTICE, + (errcode(ERRCODE_NO_DATA_FOUND), + errmsg("no matching relations in tablespace \"%s\" found", + orig_tablespaceoid == InvalidOid ? "(database default)" : + get_tablespace_name(orig_tablespaceoid)))); + + /* Everything is locked, loop through and move all of the relations. */ + foreach(l, relations) + { + List *cmds = NIL; + AlterTableCmd *cmd = makeNode(AlterTableCmd); + + cmd->subtype = AT_SetTableSpace; + cmd->name = stmt->new_tablespacename; + + cmds = lappend(cmds, cmd); + + EventTriggerAlterTableStart((Node *) stmt); + /* OID is set by AlterTableInternal */ + AlterTableInternal(lfirst_oid(l), cmds, false); + EventTriggerAlterTableEnd(); + } + + return new_tablespaceoid; +} + +static void +index_copy_data(Relation rel, RelFileLocator newrlocator) +{ + SMgrRelation dstrel; + + /* + * Since we copy the file directly without looking at the shared buffers, + * we'd better first flush out any pages of the source relation that are + * in shared buffers. We assume no new changes will be made while we are + * holding exclusive lock on the rel. + */ + FlushRelationBuffers(rel); + + /* + * Create and copy all forks of the relation, and schedule unlinking of + * old physical files. + * + * NOTE: any conflict in relfilenumber value will be caught in + * RelationCreateStorage(). + */ + dstrel = RelationCreateStorage(newrlocator, rel->rd_rel->relpersistence, true); + + /* copy main fork */ + RelationCopyStorage(RelationGetSmgr(rel), dstrel, MAIN_FORKNUM, + rel->rd_rel->relpersistence); + + /* copy those extra forks that exist */ + for (ForkNumber forkNum = MAIN_FORKNUM + 1; + forkNum <= MAX_FORKNUM; forkNum++) + { + if (smgrexists(RelationGetSmgr(rel), forkNum)) + { + smgrcreate(dstrel, forkNum, false); + + /* + * WAL log creation if the relation is persistent, or this is the + * init fork of an unlogged relation. + */ + if (RelationIsPermanent(rel) || + (rel->rd_rel->relpersistence == RELPERSISTENCE_UNLOGGED && + forkNum == INIT_FORKNUM)) + log_smgrcreate(&newrlocator, forkNum); + RelationCopyStorage(RelationGetSmgr(rel), dstrel, forkNum, + rel->rd_rel->relpersistence); + } + } - /* Can't alter a system attribute */ - if (attnum <= 0) + /* drop old relation, and close new one */ + RelationDropStorage(rel); + smgrclose(dstrel); +} + +/* + * ALTER TABLE ENABLE/DISABLE TRIGGER + * + * We just pass this off to trigger.c. + */ +static void +ATExecEnableDisableTrigger(Relation rel, const char *trigname, + char fires_when, bool skip_system, bool recurse, + LOCKMODE lockmode) +{ + EnableDisableTrigger(rel, trigname, InvalidOid, + fires_when, skip_system, recurse, + lockmode); + + InvokeObjectPostAlterHook(RelationRelationId, + RelationGetRelid(rel), 0); +} + +/* + * ALTER TABLE ENABLE/DISABLE RULE + * + * We just pass this off to rewriteDefine.c. + */ +static void +ATExecEnableDisableRule(Relation rel, const char *rulename, + char fires_when, LOCKMODE lockmode) +{ + EnableDisableRule(rel, rulename, fires_when); + + InvokeObjectPostAlterHook(RelationRelationId, + RelationGetRelid(rel), 0); +} + +/* + * Preparation phase of [NO] INHERIT + * + * Check the relation defined as a child. + */ +static void +ATPrepChangeInherit(Relation child_rel) +{ + if (child_rel->rd_rel->reloftype) ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot alter system column \"%s\"", colName), - parser_errposition(pstate, def->location))); + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot change inheritance of typed table"))); + + if (child_rel->rd_rel->relispartition) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot change inheritance of a partition"))); +} + +/* + * ALTER TABLE INHERIT + * + * Return the address of the new parent relation. + */ +static ObjectAddress +ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode) +{ + Relation parent_rel; + List *children; + ObjectAddress address; + const char *trigger_name; /* - * Cannot specify USING when altering type of a generated column, because - * that would violate the generation expression. + * A self-exclusive lock is needed here. See the similar case in + * MergeAttributes() for a full explanation. */ - 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), - parser_errposition(pstate, def->location))); + parent_rel = table_openrv(parent, ShareUpdateExclusiveLock); /* - * 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). + * Must be owner of both parent and child -- child was checked by + * ATSimplePermissions call in ATPrepCmd */ - if (attTup->attinhcount > 0 && !recursing) - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("cannot alter inherited column \"%s\"", colName), - parser_errposition(pstate, def->location))); + ATSimplePermissions(AT_AddInherit, parent_rel, + ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); - /* Don't alter columns used in the partition key */ - if (has_partition_attrs(rel, - bms_make_singleton(attnum - FirstLowInvalidHeapAttributeNumber), - &is_expr)) + /* Permanent rels cannot inherit from temporary ones */ + if (parent_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP && + child_rel->rd_rel->relpersistence != RELPERSISTENCE_TEMP) ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("cannot alter column \"%s\" because it is part of the partition key of relation \"%s\"", - colName, RelationGetRelationName(rel)), - parser_errposition(pstate, def->location))); - - /* Look up the target type */ - typenameTypeIdAndMod(pstate, typeName, &targettype, &targettypmod); - - aclresult = object_aclcheck(TypeRelationId, targettype, GetUserId(), ACL_USAGE); - if (aclresult != ACLCHECK_OK) - aclcheck_error_type(aclresult, targettype); + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot inherit from temporary relation \"%s\"", + RelationGetRelationName(parent_rel)))); - /* And the collation */ - targetcollid = GetColumnDefCollation(pstate, def, targettype); + /* If parent rel is temp, it must belong to this session */ + if (RELATION_IS_OTHER_TEMP(parent_rel)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot inherit from temporary relation of another session"))); - /* make sure datatype is legal for a column */ - CheckAttributeType(colName, targettype, targetcollid, - list_make1_oid(rel->rd_rel->reltype), - (attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL ? CHKATYPE_IS_VIRTUAL : 0)); + /* Ditto for the child */ + if (RELATION_IS_OTHER_TEMP(child_rel)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot inherit to temporary relation of another session"))); - if (attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) - { - /* do nothing */ - } - else if (tab->relkind == RELKIND_RELATION || - tab->relkind == RELKIND_PARTITIONED_TABLE) - { - /* - * Set up an expression to transform the old data value to the new - * type. If a USING option was given, use the expression as - * transformed by transformAlterTableStmt, else just take the old - * value and try to coerce it. We do this first so that type - * incompatibility can be detected before we waste effort, and because - * we need the expression to be parsed against the original table row - * type. - */ - if (!transform) - { - transform = (Node *) makeVar(1, attnum, - attTup->atttypid, attTup->atttypmod, - attTup->attcollation, - 0); - } + /* Prevent partitioned tables from becoming inheritance parents */ + if (parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot inherit from partitioned table \"%s\"", + parent->relname))); - transform = coerce_to_target_type(pstate, - transform, exprType(transform), - targettype, targettypmod, - COERCION_ASSIGNMENT, - COERCE_IMPLICIT_CAST, - -1); - if (transform == NULL) - { - /* error text depends on whether USING was specified or not */ - if (def->cooked_default != NULL) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("result of USING clause for column \"%s\"" - " cannot be cast automatically to type %s", - colName, format_type_be(targettype)), - errhint("You might need to add an explicit cast."))); - else - ereport(ERROR, - (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)) : 0)); - } + /* Likewise for partitions */ + if (parent_rel->rd_rel->relispartition) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot inherit from a partition"))); - /* Fix collations after all else */ - assign_expr_collations(pstate, transform); + /* + * Prevent circularity by seeing if proposed parent inherits from child. + * (In particular, this disallows making a rel inherit from itself.) + * + * This is not completely bulletproof because of race conditions: in + * multi-level inheritance trees, someone else could concurrently be + * making another inheritance link that closes the loop but does not join + * either of the rels we have locked. Preventing that seems to require + * exclusive locks on the entire inheritance tree, which is a cure worse + * than the disease. find_all_inheritors() will cope with circularity + * anyway, so don't sweat it too much. + * + * We use weakest lock we can on child's children, namely AccessShareLock. + */ + children = find_all_inheritors(RelationGetRelid(child_rel), + AccessShareLock, NULL); - /* Expand virtual generated columns in the expr. */ - transform = expand_generated_columns_in_expr(transform, rel, 1); + if (list_member_oid(children, RelationGetRelid(parent_rel))) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("circular inheritance not allowed"), + errdetail("\"%s\" is already a child of \"%s\".", + parent->relname, + RelationGetRelationName(child_rel)))); - /* Plan the expr now so we can accurately assess the need to rewrite. */ - transform = (Node *) expression_planner((Expr *) transform); + /* + * If child_rel has row-level triggers with transition tables, we + * currently don't allow it to become an inheritance child. See also + * prohibitions in ATExecAttachPartition() and CreateTrigger(). + */ + trigger_name = FindTriggerIncompatibleWithInheritance(child_rel->trigdesc); + if (trigger_name != NULL) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("trigger \"%s\" prevents table \"%s\" from becoming an inheritance child", + trigger_name, RelationGetRelationName(child_rel)), + errdetail("ROW triggers with transition tables are not supported in inheritance hierarchies."))); - /* - * Add a work queue item to make ATRewriteTable update the column - * contents. - */ - newval = palloc0_object(NewColumnValue); - newval->attnum = attnum; - newval->expr = (Expr *) transform; - newval->is_generated = false; + /* OK to create inheritance */ + CreateInheritance(child_rel, parent_rel, false); - tab->newvals = lappend(tab->newvals, newval); - if (ATColumnChangeRequiresRewrite(transform, attnum)) - tab->rewrite |= AT_REWRITE_COLUMN_REWRITE; - } - else if (transform) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is not a table", - RelationGetRelationName(rel)))); + ObjectAddressSet(address, RelationRelationId, + RelationGetRelid(parent_rel)); - if (!RELKIND_HAS_STORAGE(tab->relkind) || attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) - { - /* - * For relations or columns without storage, do this check now. - * Regular tables will check it later when the table is being - * rewritten. - */ - find_composite_type_dependencies(rel->rd_rel->reltype, rel, NULL); - } + /* keep our lock on the parent relation until commit */ + table_close(parent_rel, NoLock); - ReleaseSysCache(tuple); + return address; +} + +/* + * CreateInheritance + * Catalog manipulation portion of creating inheritance between a child + * table and a parent table. + * + * This verifies that all the columns and check constraints of the parent + * appear in the child and that they have the same data types and expressions. + * + * Common to ATExecAddInherit() and ATExecAttachPartition(). + */ +static void +CreateInheritance(Relation child_rel, Relation parent_rel, bool ispartition) +{ + Relation catalogRelation; + SysScanDesc scan; + ScanKeyData key; + HeapTuple inheritsTuple; + int32 inhseqno; + + /* Note: get RowExclusiveLock because we will write pg_inherits below. */ + catalogRelation = table_open(InheritsRelationId, RowExclusiveLock); /* - * Recurse manually by queueing a new command for each child, if - * necessary. We cannot apply ATSimpleRecursion here because we need to - * remap attribute numbers in the USING expression, if any. + * Check for duplicates in the list of parents, and determine the highest + * inhseqno already present; we'll use the next one for the new parent. + * Also, if proposed child is a partition, it cannot already be + * inheriting. * - * If we are told not to recurse, there had better not be any child - * tables; else the alter would put them out of step. + * Note: we do not reject the case where the child already inherits from + * the parent indirectly; CREATE TABLE doesn't reject comparable cases. */ - if (recurse) - { - Oid relid = RelationGetRelid(rel); - List *child_oids, - *child_numparents; - ListCell *lo, - *li; - - child_oids = find_all_inheritors(relid, lockmode, - &child_numparents); + ScanKeyInit(&key, + Anum_pg_inherits_inhrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(child_rel))); + scan = systable_beginscan(catalogRelation, InheritsRelidSeqnoIndexId, + true, NULL, 1, &key); - /* - * find_all_inheritors does the recursive search of the inheritance - * hierarchy, so all we have to do is process all of the relids in the - * list that it returns. - */ - forboth(lo, child_oids, li, child_numparents) - { - Oid childrelid = lfirst_oid(lo); - int numparents = lfirst_int(li); - Relation childrel; - HeapTuple childtuple; - Form_pg_attribute childattTup; + /* inhseqno sequences start at 1 */ + inhseqno = 0; + while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan))) + { + Form_pg_inherits inh = (Form_pg_inherits) GETSTRUCT(inheritsTuple); - if (childrelid == relid) - continue; + if (inh->inhparent == RelationGetRelid(parent_rel)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("relation \"%s\" would be inherited from more than once", + RelationGetRelationName(parent_rel)))); - /* find_all_inheritors already got lock */ - childrel = relation_open(childrelid, NoLock); - CheckAlterTableIsSafe(childrel); + if (inh->inhseqno > inhseqno) + inhseqno = inh->inhseqno; + } + systable_endscan(scan); - /* - * Verify that the child doesn't have any inherited definitions of - * this column that came from outside this inheritance hierarchy. - * (renameatt makes a similar test, though in a different way - * because of its different recursion mechanism.) - */ - childtuple = SearchSysCacheAttName(RelationGetRelid(childrel), - colName); - if (!HeapTupleIsValid(childtuple)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("column \"%s\" of relation \"%s\" does not exist", - colName, RelationGetRelationName(childrel)))); - childattTup = (Form_pg_attribute) GETSTRUCT(childtuple); + /* Match up the columns and bump attinhcount as needed */ + MergeAttributesIntoExisting(child_rel, parent_rel, ispartition); - if (childattTup->attinhcount > numparents) - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("cannot alter inherited column \"%s\" of relation \"%s\"", - colName, RelationGetRelationName(childrel)))); + /* Match up the constraints and bump coninhcount as needed */ + MergeConstraintsIntoExisting(child_rel, parent_rel); - ReleaseSysCache(childtuple); + /* + * OK, it looks valid. Make the catalog entries that show inheritance. + */ + StoreCatalogInheritance1(RelationGetRelid(child_rel), + RelationGetRelid(parent_rel), + inhseqno + 1, + catalogRelation, + parent_rel->rd_rel->relkind == + RELKIND_PARTITIONED_TABLE); - /* - * Remap the attribute numbers. If no USING expression was - * specified, there is no need for this step. - */ - if (def->cooked_default) - { - AttrMap *attmap; - bool found_whole_row; + /* Now we're done with pg_inherits */ + table_close(catalogRelation, RowExclusiveLock); +} - /* create a copy to scribble on */ - cmd = copyObject(cmd); +/* + * Obtain the source-text form of the constraint expression for a check + * constraint, given its pg_constraint tuple + */ +static char * +decompile_conbin(HeapTuple contup, TupleDesc tupdesc) +{ + Form_pg_constraint con; + bool isnull; + Datum attr; + Datum expr; - attmap = build_attrmap_by_name(RelationGetDescr(childrel), - RelationGetDescr(rel), - false); - ((ColumnDef *) cmd->def)->cooked_default = - map_variable_attnos(def->cooked_default, - 1, 0, - attmap, - InvalidOid, &found_whole_row); - if (found_whole_row) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot convert whole-row table reference"), - errdetail("USING expression contains a whole-row table reference."))); - pfree(attmap); - } - ATPrepCmd(wqueue, childrel, cmd, false, true, lockmode, context); - relation_close(childrel, NoLock); - } - } - else if (!recursing && - find_inheritance_children(RelationGetRelid(rel), NoLock) != NIL) - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("type of inherited column \"%s\" must be changed in child tables too", - colName))); + con = (Form_pg_constraint) GETSTRUCT(contup); + attr = heap_getattr(contup, Anum_pg_constraint_conbin, tupdesc, &isnull); + if (isnull) + elog(ERROR, "null conbin for constraint %u", con->oid); - if (tab->relkind == RELKIND_COMPOSITE_TYPE) - ATTypedTableRecursion(wqueue, rel, cmd, lockmode, context); + expr = DirectFunctionCall2(pg_get_expr, attr, + ObjectIdGetDatum(con->conrelid)); + return TextDatumGetCString(expr); } /* @@@ -20176,337 -15662,138 +19366,279 @@@ AtEOSubXact_on_commit_actions(bool isCo } else { - bool isnull; - - /* Get the old reloptions */ - datum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, - &isnull); - if (isnull) - datum = (Datum) 0; + /* cur_item must be preserved */ + if (oc->creating_subid == mySubid) + oc->creating_subid = parentSubid; + if (oc->deleting_subid == mySubid) + oc->deleting_subid = isCommit ? parentSubid : InvalidSubTransactionId; } + } +} + +/* + * This is intended as a callback for RangeVarGetRelidExtended(). It allows + * the relation to be locked only if (1) it's a plain or partitioned table, + * materialized view, or TOAST table and (2) the current user is the owner (or + * the superuser) or has been granted MAINTAIN. This meets the + * permission-checking needs of CLUSTER, REINDEX TABLE, and REFRESH + * MATERIALIZED VIEW; we expose it here so that it can be used by all. + */ +void +RangeVarCallbackMaintainsTable(const RangeVar *relation, + Oid relId, Oid oldRelId, void *arg) +{ + char relkind; + AclResult aclresult; + + /* Nothing to do if the relation was not found. */ + if (!OidIsValid(relId)) + return; + + /* + * If the relation does exist, check whether it's an index. But note that + * the relation might have been dropped between the time we did the name + * lookup and now. In that case, there's nothing to do. + */ + relkind = get_rel_relkind(relId); + if (!relkind) + return; + if (relkind != RELKIND_RELATION && relkind != RELKIND_TOASTVALUE && + relkind != RELKIND_MATVIEW && relkind != RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a table or materialized view", relation->relname))); + + /* Check permissions */ + aclresult = pg_class_aclcheck(relId, GetUserId(), ACL_MAINTAIN); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, + get_relkind_objtype(get_rel_relkind(relId)), + relation->relname); +} ++======= ++>>>>>>> theirs - /* - * Callback to RangeVarGetRelidExtended() for TRUNCATE processing. - */ - static void - RangeVarCallbackForTruncate(const RangeVar *relation, - Oid relId, Oid oldRelId, void *arg) - { - HeapTuple tuple; + newOptions = transformRelOptions(datum, defList, "toast", validnsps, + false, operation == AT_ResetRelOptions); - /* Nothing to do if the relation was not found. */ - if (!OidIsValid(relId)) - return; + (void) heap_reloptions(RELKIND_TOASTVALUE, newOptions, true); - tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relId)); - if (!HeapTupleIsValid(tuple)) /* should not happen */ - elog(ERROR, "cache lookup failed for relation %u", relId); + memset(repl_val, 0, sizeof(repl_val)); + memset(repl_null, false, sizeof(repl_null)); + memset(repl_repl, false, sizeof(repl_repl)); - truncate_check_rel(relId, (Form_pg_class) GETSTRUCT(tuple)); - truncate_check_perms(relId, (Form_pg_class) GETSTRUCT(tuple)); + if (newOptions != (Datum) 0) + repl_val[Anum_pg_class_reloptions - 1] = newOptions; + else + repl_null[Anum_pg_class_reloptions - 1] = true; - ReleaseSysCache(tuple); - } + repl_repl[Anum_pg_class_reloptions - 1] = true; - /* - * Callback for RangeVarGetRelidExtended(). Checks that the current user is - * the owner of the relation, or superuser. - */ - void - RangeVarCallbackOwnsRelation(const RangeVar *relation, - Oid relId, Oid oldRelId, void *arg) - { - HeapTuple tuple; + newtuple = heap_modify_tuple(tuple, RelationGetDescr(pgclass), + repl_val, repl_null, repl_repl); - /* Nothing to do if the relation was not found. */ - if (!OidIsValid(relId)) - return; + CatalogTupleUpdate(pgclass, &newtuple->t_self, newtuple); - tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relId)); - if (!HeapTupleIsValid(tuple)) /* should not happen */ - elog(ERROR, "cache lookup failed for relation %u", relId); + InvokeObjectPostAlterHookArg(RelationRelationId, + RelationGetRelid(toastrel), 0, + InvalidOid, true); - if (!object_ownercheck(RelationRelationId, relId, GetUserId())) - aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relId)), - relation->relname); + heap_freetuple(newtuple); ++<<<<<<< ours + /* + * Conflict log tables are used internally for logical replication + * conflict logging and should not be modified directly, as it could + * disrupt conflict logging. + */ + if (IsConflictLogTableClass((Form_pg_class) GETSTRUCT(tuple))) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot change conflict log table \"%s\"", + relation->relname), + errdetail("Conflict log tables are system-managed tables for logical replication conflicts."))); + + if (!allowSystemTableMods && + IsSystemClass(relId, (Form_pg_class) GETSTRUCT(tuple))) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied: \"%s\" is a system catalog", + relation->relname))); ++======= + ReleaseSysCache(tuple); ++>>>>>>> theirs - ReleaseSysCache(tuple); - } + table_close(toastrel, NoLock); + } + + table_close(pgclass, RowExclusiveLock); + } /* - * Common RangeVarGetRelid callback for rename, set schema, and alter table - * processing. + * Execute ALTER TABLE SET TABLESPACE for cases where there is no tuple + * rewriting to be done, so we just want to copy the data as fast as possible. */ static void - RangeVarCallbackForAlterRelation(const RangeVar *rv, Oid relid, Oid oldrelid, - void *arg) + ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode) { ++<<<<<<< ours + Node *stmt = (Node *) arg; + ObjectType reltype; + HeapTuple tuple; + Form_pg_class classform; + AclResult aclresult; + char relkind; + + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid)); + if (!HeapTupleIsValid(tuple)) + return; /* concurrently dropped */ + classform = (Form_pg_class) GETSTRUCT(tuple); + relkind = classform->relkind; + + /* Must own relation. */ + if (!object_ownercheck(RelationRelationId, relid, GetUserId())) + aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relid)), rv->relname); + + /* + * Conflict log tables are used internally for logical replication + * conflict logging and should not be altered directly, as it could + * disrupt conflict logging. + */ + if (IsConflictLogTableClass(classform)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot alter conflict log table \"%s\"", + rv->relname), + errdetail("Conflict log tables are system-managed tables for logical replication conflicts."))); + + /* No system table modifications unless explicitly allowed. */ + if (!allowSystemTableMods && IsSystemClass(relid, classform)) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied: \"%s\" is a system catalog", + rv->relname))); ++======= + Relation rel; + Oid reltoastrelid; + RelFileNumber newrelfilenumber; + RelFileLocator newrlocator; + List *reltoastidxids = NIL; + ListCell *lc; ++>>>>>>> theirs /* - * Extract the specified relation type from the statement parse tree. - * - * Also, for ALTER .. RENAME, check permissions: the user must (still) - * have CREATE rights on the containing namespace. + * Need lock here in case we are recursing to toast table or index */ - if (IsA(stmt, RenameStmt)) + rel = relation_open(tableOid, lockmode); + + /* Check first if relation can be moved to new tablespace */ + if (!CheckRelationTableSpaceMove(rel, newTableSpace)) { - aclresult = object_aclcheck(NamespaceRelationId, classform->relnamespace, - GetUserId(), ACL_CREATE); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_SCHEMA, - get_namespace_name(classform->relnamespace)); - reltype = ((RenameStmt *) stmt)->renameType; + InvokeObjectPostAlterHook(RelationRelationId, + RelationGetRelid(rel), 0); + relation_close(rel, NoLock); + return; } - else if (IsA(stmt, AlterObjectSchemaStmt)) - reltype = ((AlterObjectSchemaStmt *) stmt)->objectType; - else if (IsA(stmt, AlterTableStmt)) - reltype = ((AlterTableStmt *) stmt)->objtype; - else + reltoastrelid = rel->rd_rel->reltoastrelid; + /* Fetch the list of indexes on toast relation if necessary */ + if (OidIsValid(reltoastrelid)) { - elog(ERROR, "unrecognized node type: %d", (int) nodeTag(stmt)); - reltype = OBJECT_TABLE; /* placate compiler */ + Relation toastRel = relation_open(reltoastrelid, lockmode); + + reltoastidxids = RelationGetIndexList(toastRel); + relation_close(toastRel, lockmode); } /* - * For compatibility with prior releases, we allow ALTER TABLE to be used - * with most other types of relations (but not composite types). We allow - * similar flexibility for ALTER INDEX in the case of RENAME, but not - * otherwise. Otherwise, the user must select the correct form of the - * command for the relation at issue. + * Relfilenumbers are not unique in databases across tablespaces, so we + * need to allocate a new one in the new tablespace. */ ++<<<<<<< ours + if (reltype == OBJECT_SEQUENCE && relkind != RELKIND_SEQUENCE) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a sequence", rv->relname))); + + if (reltype == OBJECT_VIEW && relkind != RELKIND_VIEW) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a view", rv->relname))); + + if (reltype == OBJECT_MATVIEW && relkind != RELKIND_MATVIEW) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a materialized view", rv->relname))); + + if (reltype == OBJECT_FOREIGN_TABLE && relkind != RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a foreign table", rv->relname))); + + if (reltype == OBJECT_TYPE && relkind != RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a composite type", rv->relname))); + + if (reltype == OBJECT_PROPGRAPH && relkind != RELKIND_PROPGRAPH) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a property graph", rv->relname))); + + if (reltype == OBJECT_INDEX && relkind != RELKIND_INDEX && + relkind != RELKIND_PARTITIONED_INDEX + && !IsA(stmt, RenameStmt)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not an index", rv->relname))); ++======= + newrelfilenumber = GetNewRelFileNumber(newTableSpace, NULL, + rel->rd_rel->relpersistence); ++>>>>>>> theirs - /* - * Don't allow ALTER TABLE on composite types. We want people to use ALTER - * TYPE for that. - */ - if (reltype != OBJECT_TYPE && relkind == RELKIND_COMPOSITE_TYPE) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is a composite type", rv->relname), - /* translator: %s is an SQL ALTER command */ - errhint("Use %s instead.", - "ALTER TYPE"))); + /* Open old and new relation */ + newrlocator = rel->rd_locator; + newrlocator.relNumber = newrelfilenumber; + newrlocator.spcOid = newTableSpace; - /* - * Don't allow ALTER TABLE .. SET SCHEMA on relations that can't be moved - * to a different schema, such as indexes and TOAST tables. - */ - if (IsA(stmt, AlterObjectSchemaStmt)) + /* hand off to AM to actually create new rel storage and copy the data */ + if (rel->rd_rel->relkind == RELKIND_INDEX) { - if (relkind == RELKIND_INDEX || relkind == RELKIND_PARTITIONED_INDEX) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot change schema of index \"%s\"", - rv->relname), - errhint("Change the schema of the table instead."))); - else if (relkind == RELKIND_COMPOSITE_TYPE) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot change schema of composite type \"%s\"", - rv->relname), - /* translator: %s is an SQL ALTER command */ - errhint("Use %s instead.", - "ALTER TYPE"))); - else if (relkind == RELKIND_TOASTVALUE) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot change schema of TOAST table \"%s\"", - rv->relname), - errhint("Change the schema of the table instead."))); + index_copy_data(rel, newrlocator); + } + else + { + Assert(RELKIND_HAS_TABLE_AM(rel->rd_rel->relkind)); + table_relation_copy_data(rel, &newrlocator); } - - ReleaseSysCache(tuple); - } - - /* - * Transform any expressions present in the partition key - * - * Returns a transformed PartitionSpec. - */ - static PartitionSpec * - transformPartitionSpec(Relation rel, PartitionSpec *partspec) - { - PartitionSpec *newspec; - ParseState *pstate; - ParseNamespaceItem *nsitem; - ListCell *l; - - newspec = makeNode(PartitionSpec); - - newspec->strategy = partspec->strategy; - newspec->partParams = NIL; - newspec->location = partspec->location; - - /* Check valid number of columns for strategy */ - if (partspec->strategy == PARTITION_STRATEGY_LIST && - list_length(partspec->partParams) != 1) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("cannot use \"list\" partition strategy with more than one column"))); /* - * Create a dummy ParseState and insert the target relation as its sole - * rangetable entry. We need a ParseState for transformExpr. + * Update the pg_class row. + * + * NB: This wouldn't work if ATExecSetTableSpace() were allowed to be + * executed on pg_class or its indexes (the above copy wouldn't contain + * the updated pg_class entry), but that's forbidden with + * CheckRelationTableSpaceMove(). */ - pstate = make_parsestate(NULL); - nsitem = addRangeTableEntryForRelation(pstate, rel, AccessShareLock, - NULL, false, true); - addNSItemToQuery(pstate, nsitem, true, true, true); + SetRelationTableSpace(rel, newTableSpace, newrelfilenumber); - /* take care of any partition expressions */ - foreach(l, partspec->partParams) - { - PartitionElem *pelem = lfirst_node(PartitionElem, l); + InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0); - if (pelem->expr) - { - /* Copy, to avoid scribbling on the input */ - pelem = copyObject(pelem); + RelationAssumeNewRelfilelocator(rel); - /* Now do parse transformation of the expression */ - pelem->expr = transformExpr(pstate, pelem->expr, - EXPR_KIND_PARTITION_EXPRESSION); + relation_close(rel, NoLock); - /* we have to fix its collations too */ - assign_expr_collations(pstate, pelem->expr); - } + /* Make sure the reltablespace change is visible */ + CommandCounterIncrement(); - newspec->partParams = lappend(newspec->partParams, pelem); - } + /* Move associated toast relation and/or indexes, too */ + if (OidIsValid(reltoastrelid)) + ATExecSetTableSpace(reltoastrelid, newTableSpace, lockmode); + foreach(lc, reltoastidxids) + ATExecSetTableSpace(lfirst_oid(lc), newTableSpace, lockmode); - return newspec; + /* Clean up */ + list_free(reltoastidxids); } /* @@@ -21008,222 -16090,112 +19935,208 @@@ ATExecEnableDisableRule(Relation rel, c } /* - * ALTER TABLE ATTACH PARTITION FOR VALUES + * ALTER TABLE INHERIT * - * Return the address of the newly attached partition. + * Add a parent to the child's parents. This verifies that all the columns and + * check constraints of the parent appear in the child and that they have the + * same data types and expressions. */ - static ObjectAddress - ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd, - AlterTableUtilityContext *context) + static void + ATPrepAddInherit(Relation child_rel) { ++<<<<<<< ours + Relation attachrel, + catalog; + List *attachrel_children; + List *partConstraint; + SysScanDesc scan; + ScanKeyData skey; + AttrNumber attno; + int natts; + TupleDesc tupleDesc; + ObjectAddress address; + const char *trigger_name; + Oid defaultPartOid; + List *partBoundConstraint; + List *exceptpuboids = NIL; + ParseState *pstate = make_parsestate(NULL); ++======= + if (child_rel->rd_rel->reloftype) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot change inheritance of typed table"))); ++>>>>>>> theirs - pstate->p_sourcetext = context->queryString; + if (child_rel->rd_rel->relispartition) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot change inheritance of a partition"))); - /* - * We must lock the default partition if one exists, because attaching a - * new partition will change its partition constraint. - */ - defaultPartOid = - get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true)); - if (OidIsValid(defaultPartOid)) - LockRelationOid(defaultPartOid, AccessExclusiveLock); + if (child_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot change inheritance of partitioned table"))); + } - attachrel = table_openrv(cmd->name, AccessExclusiveLock); + /* + * Return the address of the new parent relation. + */ + static ObjectAddress + ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode) + { + Relation parent_rel; + List *children; + ObjectAddress address; + const char *trigger_name; /* - * XXX I think it'd be a good idea to grab locks on all tables referenced - * by FKs at this point also. + * A self-exclusive lock is needed here. See the similar case in + * MergeAttributes() for a full explanation. */ + parent_rel = table_openrv(parent, ShareUpdateExclusiveLock); /* - * Must be owner of both parent and source table -- parent was checked by + * Must be owner of both parent and child -- child was checked by * ATSimplePermissions call in ATPrepCmd */ - ATSimplePermissions(AT_AttachPartition, attachrel, + ATSimplePermissions(AT_AddInherit, parent_rel, ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); ++<<<<<<< ours + /* A partition can only have one parent */ + if (attachrel->rd_rel->relispartition) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is already a partition", + RelationGetRelationName(attachrel)))); + + if (OidIsValid(attachrel->rd_rel->reloftype)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot attach a typed table as partition"))); + + /* + * Disallow attaching a partition if the table is referenced in a + * publication EXCEPT clause. Changing the partition hierarchy could alter + * the effective publication membership. + */ + exceptpuboids = GetRelationExcludedPublications(RelationGetRelid(attachrel)); + if (exceptpuboids != NIL) + { + bool first = true; + StringInfoData pubnames; + + initStringInfo(&pubnames); + + foreach_oid(pubid, exceptpuboids) + { + char *pubname = get_publication_name(pubid, false); + + if (first) + appendStringInfo(&pubnames, _("\"%s\""), pubname); + else + appendStringInfo(&pubnames, _(", \"%s\""), pubname); + first = false; + } + + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg_plural("cannot attach table \"%s\" as partition because it is referenced in publication %s EXCEPT clause", + "cannot attach table \"%s\" as partition because it is referenced in publications %s EXCEPT clause", + list_length(exceptpuboids), + RelationGetRelationName(attachrel), + pubnames.data), + errdetail("The publication EXCEPT clause cannot contain tables that are partitions."), + errhint("Change the publication's EXCEPT clause using ALTER PUBLICATION ... SET ALL TABLES.")); + } + + list_free(exceptpuboids); + + /* + * Table being attached should not already be part of inheritance; either + * as a child table... + */ + catalog = table_open(InheritsRelationId, AccessShareLock); + ScanKeyInit(&skey, + Anum_pg_inherits_inhrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(attachrel))); + scan = systable_beginscan(catalog, InheritsRelidSeqnoIndexId, true, + NULL, 1, &skey); + if (HeapTupleIsValid(systable_getnext(scan))) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot attach inheritance child as partition"))); + systable_endscan(scan); + + /* ...or as a parent table (except the case when it is partitioned) */ + ScanKeyInit(&skey, + Anum_pg_inherits_inhparent, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(attachrel))); + scan = systable_beginscan(catalog, InheritsParentIndexId, true, NULL, + 1, &skey); + if (HeapTupleIsValid(systable_getnext(scan)) && + attachrel->rd_rel->relkind == RELKIND_RELATION) ++======= + /* Permanent rels cannot inherit from temporary ones */ + if (parent_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP && + child_rel->rd_rel->relpersistence != RELPERSISTENCE_TEMP) ++>>>>>>> theirs ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot attach inheritance parent as partition"))); - systable_endscan(scan); - table_close(catalog, AccessShareLock); - - /* - * Prevent circularity by seeing if rel is a partition of attachrel. (In - * particular, this disallows making a rel a partition of itself.) - * - * We do that by checking if rel is a member of the list of attachrel's - * partitions provided the latter is partitioned at all. We want to avoid - * having to construct this list again, so we request the strongest lock - * on all partitions. We need the strongest lock, because we may decide - * to scan them if we find out that the table being attached (or its leaf - * partitions) may contain rows that violate the partition constraint. If - * the table has a constraint that would prevent such rows, which by - * definition is present in all the partitions, we need not scan the - * table, nor its partitions. But we cannot risk a deadlock by taking a - * weaker lock now and the stronger one only when needed. - */ - attachrel_children = find_all_inheritors(RelationGetRelid(attachrel), - AccessExclusiveLock, NULL); - if (list_member_oid(attachrel_children, RelationGetRelid(rel))) - ereport(ERROR, - (errcode(ERRCODE_DUPLICATE_TABLE), - errmsg("circular inheritance not allowed"), - errdetail("\"%s\" is already a child of \"%s\".", - RelationGetRelationName(rel), - RelationGetRelationName(attachrel)))); + errmsg("cannot inherit from temporary relation \"%s\"", + RelationGetRelationName(parent_rel)))); - /* If the parent is permanent, so must be all of its partitions. */ - if (rel->rd_rel->relpersistence != RELPERSISTENCE_TEMP && - attachrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP) + /* If parent rel is temp, it must belong to this session */ + if (RELATION_IS_OTHER_TEMP(parent_rel)) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot attach a temporary relation as partition of permanent relation \"%s\"", - RelationGetRelationName(rel)))); + errmsg("cannot inherit from temporary relation of another session"))); - /* Temp parent cannot have a partition that is itself not a temp */ - if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP && - attachrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP) + /* Ditto for the child */ + if (RELATION_IS_OTHER_TEMP(child_rel)) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot attach a permanent relation as partition of temporary relation \"%s\"", - RelationGetRelationName(rel)))); + errmsg("cannot inherit to temporary relation of another session"))); - /* If the parent is temp, it must belong to this session */ - if (RELATION_IS_OTHER_TEMP(rel)) + /* Prevent partitioned tables from becoming inheritance parents */ + if (parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot attach as partition of temporary relation of another session"))); + errmsg("cannot inherit from partitioned table \"%s\"", + parent->relname))); - /* Ditto for the partition */ - if (RELATION_IS_OTHER_TEMP(attachrel)) + /* Likewise for partitions */ + if (parent_rel->rd_rel->relispartition) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot attach temporary relation of another session as partition"))); + errmsg("cannot inherit from a partition"))); /* - * Check if attachrel has any identity columns or any columns that aren't - * in the parent. + * Prevent circularity by seeing if proposed parent inherits from child. + * (In particular, this disallows making a rel inherit from itself.) + * + * This is not completely bulletproof because of race conditions: in + * multi-level inheritance trees, someone else could concurrently be + * making another inheritance link that closes the loop but does not join + * either of the rels we have locked. Preventing that seems to require + * exclusive locks on the entire inheritance tree, which is a cure worse + * than the disease. find_all_inheritors() will cope with circularity + * anyway, so don't sweat it too much. + * + * We use weakest lock we can on child's children, namely AccessShareLock. */ - tupleDesc = RelationGetDescr(attachrel); - natts = tupleDesc->natts; - for (attno = 1; attno <= natts; attno++) - { - Form_pg_attribute attribute = TupleDescAttr(tupleDesc, attno - 1); - char *attributeName = NameStr(attribute->attname); - - /* Ignore dropped */ - if (attribute->attisdropped) - continue; + children = find_all_inheritors(RelationGetRelid(child_rel), + AccessShareLock, NULL); - if (attribute->attidentity) - ereport(ERROR, - errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("table \"%s\" being attached contains an identity column \"%s\"", - RelationGetRelationName(attachrel), attributeName), - errdetail("The new partition may not contain an identity column.")); - - /* Try to find the column in parent (matching on column name) */ - if (!SearchSysCacheExists2(ATTNAME, - ObjectIdGetDatum(RelationGetRelid(rel)), - CStringGetDatum(attributeName))) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("table \"%s\" contains column \"%s\" not found in parent \"%s\"", - RelationGetRelationName(attachrel), attributeName, - RelationGetRelationName(rel)), - errdetail("The new partition may contain only the columns present in parent."))); - } + if (list_member_oid(children, RelationGetRelid(parent_rel))) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("circular inheritance not allowed"), + errdetail("\"%s\" is already a child of \"%s\".", + parent->relname, + RelationGetRelationName(child_rel)))); /* * If child_rel has row-level triggers with transition tables, we @@@ -22299,259 -16990,149 +20931,219 @@@ drop_parent_dependency(Oid relid, Oid r } /* - * Before acquiring lock on an index, acquire the same lock on the owning - * table. + * ALTER TABLE OF + * + * Attach a table to a composite type, as though it had been created with CREATE + * TABLE OF. All attname, atttypid, atttypmod and attcollation must match. The + * subject table must not have inheritance parents. These restrictions ensure + * that you cannot create a configuration impossible with CREATE TABLE OF alone. + * + * The address of the type is returned. */ - struct AttachIndexCallbackState - { - Oid partitionOid; - Oid parentTblOid; - bool lockedParentTbl; - }; - - static void - RangeVarCallbackForAttachIndex(const RangeVar *rv, Oid relOid, Oid oldRelOid, - void *arg) + static ObjectAddress + ATExecAddOf(Relation rel, const TypeName *ofTypename, LOCKMODE lockmode) { - struct AttachIndexCallbackState *state; - Form_pg_class classform; - HeapTuple tuple; - - state = (struct AttachIndexCallbackState *) arg; - - if (!state->lockedParentTbl) - { - LockRelationOid(state->parentTblOid, AccessShareLock); - state->lockedParentTbl = true; - } - - /* - * If we previously locked some other heap, and the name we're looking up - * no longer refers to an index on that relation, release the now-useless - * lock. XXX maybe we should do *after* we verify whether the index does - * not actually belong to the same relation ... - */ - if (relOid != oldRelOid && OidIsValid(state->partitionOid)) - { - UnlockRelationOid(state->partitionOid, AccessShareLock); - state->partitionOid = InvalidOid; - } + Oid relid = RelationGetRelid(rel); + Type typetuple; + Form_pg_type typeform; + Oid typeid; + Relation inheritsRelation, + relationRelation; + SysScanDesc scan; + ScanKeyData key; + AttrNumber table_attno, + type_attno; + TupleDesc typeTupleDesc, + tableTupleDesc; + ObjectAddress tableobj, + typeobj; + HeapTuple classtuple; - /* Didn't find a relation, so no need for locking or permission checks. */ - if (!OidIsValid(relOid)) - return; + /* Validate the type. */ + typetuple = typenameType(NULL, ofTypename, NULL); + check_of_type(typetuple); + typeform = (Form_pg_type) GETSTRUCT(typetuple); + typeid = typeform->oid; - tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid)); - if (!HeapTupleIsValid(tuple)) - return; /* concurrently dropped, so nothing to do */ - classform = (Form_pg_class) GETSTRUCT(tuple); - if (classform->relkind != RELKIND_PARTITIONED_INDEX && - classform->relkind != RELKIND_INDEX) + /* Fail if the table has any inheritance parents. */ + inheritsRelation = table_open(InheritsRelationId, AccessShareLock); + ScanKeyInit(&key, + Anum_pg_inherits_inhrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relid)); + scan = systable_beginscan(inheritsRelation, InheritsRelidSeqnoIndexId, + true, NULL, 1, &key); + if (HeapTupleIsValid(systable_getnext(scan))) ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("\"%s\" is not an index", rv->relname))); - ReleaseSysCache(tuple); + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("typed tables cannot inherit"))); + systable_endscan(scan); + table_close(inheritsRelation, AccessShareLock); /* - * Since we need only examine the heap's tupledesc, an access share lock - * on it (preventing any DDL) is sufficient. + * Check the tuple descriptors for compatibility. Unlike inheritance, we + * require that the order also match. However, attnotnull need not match. */ ++<<<<<<< ours + state->partitionOid = IndexGetRelation(relOid, false); + LockRelationOid(state->partitionOid, AccessShareLock); +} + +/* + * ALTER INDEX i1 ATTACH PARTITION i2 + */ +static ObjectAddress +ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, RangeVar *name) +{ + Relation partIdx; + Relation partTbl; + Relation parentTbl; + ObjectAddress address; + Oid partIdxId; + Oid currParent; + struct AttachIndexCallbackState state; + + /* + * We need to obtain lock on the index 'name' to modify it, but we also + * need to read its owning table's tuple descriptor -- so we need to lock + * both. To avoid deadlocks, obtain lock on the table before doing so on + * the index. Furthermore, we need to examine the parent table of the + * partition, so lock that one too. + */ + state.partitionOid = InvalidOid; + state.parentTblOid = parentIdx->rd_index->indrelid; + state.lockedParentTbl = false; + partIdxId = + RangeVarGetRelidExtended(name, AccessExclusiveLock, 0, + RangeVarCallbackForAttachIndex, + &state); + /* Not there? */ + if (!OidIsValid(partIdxId)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("index \"%s\" does not exist", name->relname))); + + /* no deadlock risk: RangeVarGetRelidExtended already acquired the lock */ + partIdx = relation_open(partIdxId, AccessExclusiveLock); + + /* we already hold locks on both tables, so this is safe: */ + parentTbl = relation_open(parentIdx->rd_index->indrelid, AccessShareLock); + partTbl = relation_open(partIdx->rd_index->indrelid, NoLock); + + ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partIdx)); + + /* + * Check if the index is already attached to the correct parent, + * ultimately attempting one round of validation if already the case. + */ + currParent = partIdx->rd_rel->relispartition ? + get_partition_parent(partIdxId, false) : InvalidOid; + if (currParent != RelationGetRelid(parentIdx)) ++======= + typeTupleDesc = lookup_rowtype_tupdesc(typeid, -1); + tableTupleDesc = RelationGetDescr(rel); + table_attno = 1; + for (type_attno = 1; type_attno <= typeTupleDesc->natts; type_attno++) ++>>>>>>> theirs { - IndexInfo *childInfo; - IndexInfo *parentInfo; - AttrMap *attmap; - bool found; - int i; - PartitionDesc partDesc; - Oid constraintOid, - cldConstrId = InvalidOid; + Form_pg_attribute type_attr, + table_attr; + const char *type_attname, + *table_attname; - /* - * If this partition already has an index attached, refuse the - * operation. - */ - refuseDupeIndexAttach(parentIdx, partIdx, partTbl); + /* Get the next non-dropped type attribute. */ + type_attr = TupleDescAttr(typeTupleDesc, type_attno - 1); + if (type_attr->attisdropped) + continue; + type_attname = NameStr(type_attr->attname); - if (OidIsValid(currParent)) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("cannot attach index \"%s\" as a partition of index \"%s\"", - RelationGetRelationName(partIdx), - RelationGetRelationName(parentIdx)), - errdetail("Index \"%s\" is already attached to another index.", - RelationGetRelationName(partIdx)))); - - /* Make sure it indexes a partition of the other index's table */ - partDesc = RelationGetPartitionDesc(parentTbl, true); - found = false; - for (i = 0; i < partDesc->nparts; i++) + /* Get the next non-dropped table attribute. */ + do { - if (partDesc->oids[i] == state.partitionOid) - { - found = true; - break; - } - } - if (!found) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("cannot attach index \"%s\" as a partition of index \"%s\"", - RelationGetRelationName(partIdx), - RelationGetRelationName(parentIdx)), - errdetail("Index \"%s\" is not an index on any partition of table \"%s\".", - RelationGetRelationName(partIdx), - RelationGetRelationName(parentTbl)))); - - /* Ensure the indexes are compatible */ - childInfo = BuildIndexInfo(partIdx); - parentInfo = BuildIndexInfo(parentIdx); - attmap = build_attrmap_by_name(RelationGetDescr(partTbl), - RelationGetDescr(parentTbl), - false); - if (!CompareIndexInfo(childInfo, parentInfo, - partIdx->rd_indcollation, - parentIdx->rd_indcollation, - partIdx->rd_opfamily, - parentIdx->rd_opfamily, - attmap)) + if (table_attno > tableTupleDesc->natts) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("table is missing column \"%s\"", + type_attname))); + table_attr = TupleDescAttr(tableTupleDesc, table_attno - 1); + table_attno++; + } while (table_attr->attisdropped); + table_attname = NameStr(table_attr->attname); + + /* Compare name. */ + if (strncmp(table_attname, type_attname, NAMEDATALEN) != 0) ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("cannot attach index \"%s\" as a partition of index \"%s\"", - RelationGetRelationName(partIdx), - RelationGetRelationName(parentIdx)), - errdetail("The index definitions do not match."))); + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("table has column \"%s\" where type requires \"%s\"", + table_attname, type_attname))); - /* - * If there is a constraint in the parent, make sure there is one in - * the child too. - */ - constraintOid = get_relation_idx_constraint_oid(RelationGetRelid(parentTbl), - RelationGetRelid(parentIdx)); + /* Compare type. */ + if (table_attr->atttypid != type_attr->atttypid || + table_attr->atttypmod != type_attr->atttypmod || + table_attr->attcollation != type_attr->attcollation) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("table \"%s\" has different type for column \"%s\"", + RelationGetRelationName(rel), type_attname))); + } + ReleaseTupleDesc(typeTupleDesc); - if (OidIsValid(constraintOid)) - { - cldConstrId = get_relation_idx_constraint_oid(RelationGetRelid(partTbl), - partIdxId); - if (!OidIsValid(cldConstrId)) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("cannot attach index \"%s\" as a partition of index \"%s\"", - RelationGetRelationName(partIdx), - RelationGetRelationName(parentIdx)), - errdetail("The index \"%s\" belongs to a constraint in table \"%s\" but no constraint exists for index \"%s\".", - RelationGetRelationName(parentIdx), - RelationGetRelationName(parentTbl), - RelationGetRelationName(partIdx)))); - } + /* Any remaining columns at the end of the table had better be dropped. */ + for (; table_attno <= tableTupleDesc->natts; table_attno++) + { + Form_pg_attribute table_attr = TupleDescAttr(tableTupleDesc, + table_attno - 1); - /* - * If it's a primary key, make sure the columns in the partition are - * NOT NULL. - */ - if (parentIdx->rd_index->indisprimary) - verifyPartitionIndexNotNull(childInfo, partTbl); + if (!table_attr->attisdropped) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("table has extra column \"%s\"", + NameStr(table_attr->attname)))); + } - /* All good -- do it */ - IndexSetParentIndex(partIdx, RelationGetRelid(parentIdx)); - if (OidIsValid(constraintOid)) - ConstraintSetParentConstraint(cldConstrId, constraintOid, - RelationGetRelid(partTbl)); + /* If the table was already typed, drop the existing dependency. */ + if (rel->rd_rel->reloftype) + drop_parent_dependency(relid, TypeRelationId, rel->rd_rel->reloftype, + DEPENDENCY_NORMAL); - free_attrmap(attmap); + /* Record a dependency on the new type. */ + tableobj.classId = RelationRelationId; + tableobj.objectId = relid; + tableobj.objectSubId = 0; + typeobj.classId = TypeRelationId; + typeobj.objectId = typeid; + typeobj.objectSubId = 0; + recordDependencyOn(&tableobj, &typeobj, DEPENDENCY_NORMAL); ++<<<<<<< ours + validatePartitionedIndex(parentIdx, parentTbl); + } + else if (!parentIdx->rd_index->indisvalid) + { + /* + * The index is attached, but the parent is still invalid; see if it + * can be validated now. + */ + validatePartitionedIndex(parentIdx, parentTbl); + } ++======= + /* Update pg_class.reloftype */ + relationRelation = table_open(RelationRelationId, RowExclusiveLock); + classtuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid)); + if (!HeapTupleIsValid(classtuple)) + elog(ERROR, "cache lookup failed for relation %u", relid); + ((Form_pg_class) GETSTRUCT(classtuple))->reloftype = typeid; + CatalogTupleUpdate(relationRelation, &classtuple->t_self, classtuple); ++>>>>>>> theirs - relation_close(parentTbl, AccessShareLock); - /* keep these locks till commit */ - relation_close(partTbl, NoLock); - relation_close(partIdx, NoLock); + InvokeObjectPostAlterHook(RelationRelationId, relid, 0); - return address; - } + heap_freetuple(classtuple); + table_close(relationRelation, RowExclusiveLock); - /* - * Verify whether the given partition already contains an index attached - * to the given partitioned index. If so, raise an error. - */ - static void - refuseDupeIndexAttach(Relation parentIdx, Relation partIdx, Relation partitionTbl) - { - Oid existingIdx; + ReleaseSysCache(typetuple); - existingIdx = index_get_partition(partitionTbl, - RelationGetRelid(parentIdx)); - if (OidIsValid(existingIdx)) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("cannot attach index \"%s\" as a partition of index \"%s\"", - RelationGetRelationName(partIdx), - RelationGetRelationName(parentIdx)), - errdetail("Another index \"%s\" is already attached for partition \"%s\".", - get_rel_name(existingIdx), - RelationGetRelationName(partitionTbl)))); + return typeobj; } /* @@@ -22995,447 -17444,259 +21455,449 @@@ ATExecForceNoForceRowSecurity(Relation } /* - * createTableConstraints: - * create check constraints, default values, and generated values for newRel - * based on parent_rel. tab is pending-work queue for newRel, we may need it in - * MergePartitionsMoveRows. + * ALTER FOREIGN TABLE OPTIONS (...) */ static void - createTableConstraints(List **wqueue, AlteredTableInfo *tab, - Relation parent_rel, Relation newRel) + ATExecGenericOptions(Relation rel, List *options) { - TupleDesc tupleDesc; - TupleConstr *constr; - AttrMap *attmap; - AttrNumber parent_attno; - int ccnum; - List *constraints = NIL; - List *cookedConstraints = NIL; - - tupleDesc = RelationGetDescr(parent_rel); - constr = tupleDesc->constr; + Relation ftrel; + ForeignServer *server; + ForeignDataWrapper *fdw; + HeapTuple tuple; + bool isnull; + Datum repl_val[Natts_pg_foreign_table]; + bool repl_null[Natts_pg_foreign_table]; + bool repl_repl[Natts_pg_foreign_table]; + Datum datum; + Form_pg_foreign_table tableform; - if (!constr) + if (options == NIL) return; - /* - * Construct a map from the parent relation's attnos to the child rel's. - * This re-checks type match, etc, although it shouldn't be possible to - * have a failure since both tables are locked. - */ - attmap = build_attrmap_by_name(RelationGetDescr(newRel), - tupleDesc, - false); - - /* Cycle for default values. */ - for (parent_attno = 1; parent_attno <= tupleDesc->natts; parent_attno++) - { - Form_pg_attribute attribute = TupleDescAttr(tupleDesc, - parent_attno - 1); - - /* Ignore dropped columns in the parent. */ - if (attribute->attisdropped) - continue; - - /* Copy the default, if present, and it should be copied. */ - if (attribute->atthasdef) - { - Node *this_default = NULL; - bool found_whole_row; - AttrNumber num; - Node *def; - NewColumnValue *newval; + ftrel = table_open(ForeignTableRelationId, RowExclusiveLock); - if (attribute->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) - this_default = build_generation_expression(parent_rel, attribute->attnum); - else - { - this_default = TupleDescGetDefault(tupleDesc, attribute->attnum); - if (this_default == NULL) - elog(ERROR, "default expression not found for attribute %d of relation \"%s\"", - attribute->attnum, RelationGetRelationName(parent_rel)); - } + tuple = SearchSysCacheCopy1(FOREIGNTABLEREL, + ObjectIdGetDatum(rel->rd_id)); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("foreign table \"%s\" does not exist", + RelationGetRelationName(rel)))); + tableform = (Form_pg_foreign_table) GETSTRUCT(tuple); + server = GetForeignServer(tableform->ftserver); + fdw = GetForeignDataWrapper(server->fdwid); - num = attmap->attnums[parent_attno - 1]; - def = map_variable_attnos(this_default, 1, 0, attmap, InvalidOid, &found_whole_row); + memset(repl_val, 0, sizeof(repl_val)); + memset(repl_null, false, sizeof(repl_null)); + memset(repl_repl, false, sizeof(repl_repl)); - if (found_whole_row && attribute->attgenerated != '\0') - elog(ERROR, "cannot convert whole-row table reference"); + /* Extract the current options */ + datum = SysCacheGetAttr(FOREIGNTABLEREL, + tuple, + Anum_pg_foreign_table_ftoptions, + &isnull); + if (isnull) + datum = PointerGetDatum(NULL); ++<<<<<<< ours + /* Add a pre-cooked default expression. */ + StoreAttrDefault(newRel, num, def, false); ++======= + /* Transform the options */ + datum = transformGenericOptions(ForeignTableRelationId, + datum, + options, + fdw->fdwvalidator); ++>>>>>>> theirs + + if (DatumGetPointer(datum) != NULL) + repl_val[Anum_pg_foreign_table_ftoptions - 1] = datum; + else + repl_null[Anum_pg_foreign_table_ftoptions - 1] = true; - /* - * Stored generated column expressions in parent_rel might - * reference the tableoid. newRel, parent_rel tableoid clear is - * not the same. If so, these stored generated columns require - * recomputation for newRel within MergePartitionsMoveRows. - */ - if (attribute->attgenerated == ATTRIBUTE_GENERATED_STORED) - { - newval = palloc0_object(NewColumnValue); - newval->attnum = num; - newval->expr = expression_planner((Expr *) def); - newval->is_generated = (attribute->attgenerated != '\0'); - tab->newvals = lappend(tab->newvals, newval); - } - } - } - ++<<<<<<< ours + /* Cycle for CHECK constraints. */ + for (ccnum = 0; ccnum < constr->num_check; ccnum++) + { + char *ccname = constr->check[ccnum].ccname; + char *ccbin = constr->check[ccnum].ccbin; + bool ccenforced = constr->check[ccnum].ccenforced; + bool ccnoinherit = constr->check[ccnum].ccnoinherit; + bool ccvalid = constr->check[ccnum].ccvalid; + Node *ccbin_node; + bool found_whole_row; + Constraint *con; ++======= + repl_repl[Anum_pg_foreign_table_ftoptions - 1] = true; ++>>>>>>> theirs - /* - * The partitioned table can not have a NO INHERIT check constraint - * (see StoreRelCheck function for details). - */ - Assert(!ccnoinherit); + /* Everything looks good - update the tuple */ - ccbin_node = map_variable_attnos(stringToNode(ccbin), - 1, 0, - attmap, - InvalidOid, &found_whole_row); + tuple = heap_modify_tuple(tuple, RelationGetDescr(ftrel), + repl_val, repl_null, repl_repl); ++<<<<<<< ours + /* + * For the moment we have to reject whole-row variables (as for CREATE + * TABLE LIKE and inheritances). + */ + if (found_whole_row) + elog(ERROR, "Constraint \"%s\" contains a whole-row reference to table \"%s\".", + ccname, + RelationGetRelationName(parent_rel)); + + con = makeNode(Constraint); + con->contype = CONSTR_CHECK; + con->conname = pstrdup(ccname); + con->deferrable = false; + con->initdeferred = false; + con->is_enforced = ccenforced; + con->skip_validation = !ccvalid; + con->initially_valid = ccvalid; + con->is_no_inherit = ccnoinherit; + con->raw_expr = NULL; + con->cooked_expr = nodeToString(ccbin_node); + con->location = -1; + constraints = lappend(constraints, con); + } + + /* Install all CHECK constraints. */ + cookedConstraints = AddRelationNewConstraints(newRel, NIL, constraints, + false, true, false, NULL); + + /* Make the additional catalog changes visible. */ + CommandCounterIncrement(); ++======= + CatalogTupleUpdate(ftrel, &tuple->t_self, tuple); ++>>>>>>> theirs /* - * parent_rel check constraint expression may reference tableoid, so later - * in MergePartitionsMoveRows, we need to evaluate the check constraint - * again for the newRel. We can check whether the check constraint - * contains a tableoid reference via pull_varattnos. + * Invalidate relcache so that all sessions will refresh any cached plans + * that might depend on the old options. */ - foreach_ptr(CookedConstraint, ccon, cookedConstraints) - { - if (!ccon->skip_validation) - { - Node *qual; - Bitmapset *attnums = NULL; - - Assert(ccon->contype == CONSTR_CHECK); - qual = expand_generated_columns_in_expr(ccon->expr, newRel, 1); - pull_varattnos(qual, 1, &attnums); - - /* - * Add a check only if it contains a tableoid - * (TableOidAttributeNumber). - */ - if (bms_is_member(TableOidAttributeNumber - FirstLowInvalidHeapAttributeNumber, - attnums)) - { - NewConstraint *newcon; - - newcon = palloc0_object(NewConstraint); - newcon->name = ccon->name; - newcon->contype = CONSTR_CHECK; - newcon->qual = qual; - - tab->constraints = lappend(tab->constraints, newcon); - } - } - } - - /* Don't need the cookedConstraints anymore. */ - list_free_deep(cookedConstraints); - - /* Reproduce not-null constraints. */ - if (constr->has_not_null) - { - List *nnconstraints; - - /* - * The "include_noinh" argument is false because a partitioned table - * can't have NO INHERIT constraint. - */ - nnconstraints = RelationGetNotNullConstraints(RelationGetRelid(parent_rel), - false, false); + CacheInvalidateRelcache(rel); - Assert(list_length(nnconstraints) > 0); + InvokeObjectPostAlterHook(ForeignTableRelationId, + RelationGetRelid(rel), 0); ++<<<<<<< ours + /* + * We already set pg_attribute.attnotnull in createPartitionTable. No + * need call set_attnotnull again. + */ + AddRelationNewConstraints(newRel, NIL, nnconstraints, false, true, false, NULL); + } ++======= + table_close(ftrel, RowExclusiveLock); + + heap_freetuple(tuple); ++>>>>>>> theirs } /* - * createPartitionTable: - * - * Create a new partition (newPartName) for the partitioned table (parent_rel). - * ownerId is determined by the partition on which the operation is performed, - * so it is passed separately. The new partition will inherit the access method - * and persistence type from the parent table. + * ALTER TABLE ALTER COLUMN SET COMPRESSION * - * Returns the created relation (locked in AccessExclusiveLock mode). + * Return value is the address of the modified column */ - static Relation - createPartitionTable(List **wqueue, RangeVar *newPartName, - Relation parent_rel, Oid ownerId) + static ObjectAddress + ATExecSetCompression(Relation rel, + const char *column, + Node *newValue, + LOCKMODE lockmode) { ++<<<<<<< ours + Relation newRel; + Oid newRelId; + Oid existingRelid; + Oid tablespaceId; + TupleDesc descriptor; + List *colList = NIL; + Oid relamId; + Oid namespaceId; + AlteredTableInfo *new_partrel_tab; + Form_pg_class parent_relform = parent_rel->rd_rel; + + /* If the existing rel is temp, it must belong to this session. */ + if (RELATION_IS_OTHER_TEMP(parent_rel)) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot create as partition of temporary relation of another session")); ++======= + Relation attrel; + HeapTuple tuple; + Form_pg_attribute atttableform; + AttrNumber attnum; + char *compression; + char cmethod; + ObjectAddress address; ++>>>>>>> theirs - /* Look up inheritance ancestors and generate the relation schema. */ - colList = getAttributesList(parent_rel); + compression = strVal(newValue); - /* Create a tuple descriptor from the relation schema. */ - descriptor = BuildDescForRelation(colList); + attrel = table_open(AttributeRelationId, RowExclusiveLock); - /* Look up the access method for the new relation. */ - relamId = (parent_relform->relam != InvalidOid) ? parent_relform->relam : HEAP_TABLE_AM_OID; + /* copy the cache entry so we can scribble on it below */ + tuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), column); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + column, RelationGetRelationName(rel)))); - /* Look up the namespace in which we are supposed to create the relation. */ - namespaceId = - RangeVarGetAndCheckCreationNamespace(newPartName, NoLock, &existingRelid); - if (OidIsValid(existingRelid)) + /* prevent them from altering a system attribute */ + atttableform = (Form_pg_attribute) GETSTRUCT(tuple); + attnum = atttableform->attnum; + if (attnum <= 0) ereport(ERROR, - errcode(ERRCODE_DUPLICATE_TABLE), - errmsg("relation \"%s\" already exists", newPartName->relname)); + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter system column \"%s\"", column))); /* - * We intended to create the partition with the same persistence as the - * parent table, but we still need to recheck because that might be - * affected by the search_path. If the parent is permanent, so must be - * all of its partitions. + * Check that column type is compressible, then get the attribute + * compression method code */ - if (parent_relform->relpersistence != RELPERSISTENCE_TEMP && - newPartName->relpersistence == RELPERSISTENCE_TEMP) - ereport(ERROR, - errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot create a temporary relation as partition of permanent relation \"%s\"", - RelationGetRelationName(parent_rel))); + cmethod = GetAttributeCompression(atttableform->atttypid, compression); ++<<<<<<< ours + /* Permanent rels cannot be partitions belonging to a temporary parent. */ + if (newPartName->relpersistence != RELPERSISTENCE_TEMP && + parent_relform->relpersistence == RELPERSISTENCE_TEMP) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot create a permanent relation as partition of temporary relation \"%s\"", + RelationGetRelationName(parent_rel))); + + /* + * Select the tablespace for the new partition. Mirror the logic that + * CREATE TABLE foo PARTITION OF ... uses in DefineRelation: take the + * partitioned parent's explicit tablespace if it has one, otherwise take + * default_tablespace into account, and finally use the database default. + */ + tablespaceId = parent_relform->reltablespace; + if (!OidIsValid(tablespaceId)) + tablespaceId = GetDefaultTablespace(newPartName->relpersistence, false); + + /* Check permissions except when using database's default */ + if (OidIsValid(tablespaceId) && tablespaceId != MyDatabaseTableSpace) + { + AclResult aclresult; + + aclresult = object_aclcheck(TableSpaceRelationId, tablespaceId, + GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tablespaceId)); + } + + /* In all cases disallow placing user relations in pg_global */ + if (tablespaceId == GLOBALTABLESPACE_OID) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("only shared relations can be placed in pg_global tablespace"))); + + /* Create the relation. */ + newRelId = heap_create_with_catalog(newPartName->relname, + namespaceId, + tablespaceId, + InvalidOid, + InvalidOid, + InvalidOid, + ownerId, + relamId, + descriptor, + NIL, + RELKIND_RELATION, + newPartName->relpersistence, + false, + false, + ONCOMMIT_NOOP, + (Datum) 0, + true, + allowSystemTableMods, + false, /* is_internal */ + InvalidOid, + NULL); ++======= + /* update pg_attribute entry */ + atttableform->attcompression = cmethod; + CatalogTupleUpdate(attrel, &tuple->t_self, tuple); ++>>>>>>> theirs - /* - * We must bump the command counter to make the newly-created relation - * tuple visible for opening. - */ - CommandCounterIncrement(); + InvokeObjectPostAlterHook(RelationRelationId, + RelationGetRelid(rel), + attnum); /* ++<<<<<<< ours + * Create a TOAST table if the table needs one. MERGE/SPLIT PARTITION + * moves rows from existing partition(s) into new partition(s), which may + * carry out-of-line varlena values that the new relation must be able to + * store. Also, the new partition must be able to receive out-of-line + * varlena values after the DDL operation is complete. + */ + NewRelationCreateToastTable(newRelId, (Datum) 0); + + /* + * Open the new partition with no lock, because we already have an + * AccessExclusiveLock placed there after creation. ++======= + * Apply the change to indexes as well (only for simple index columns, + * matching behavior of index.c ConstructTupleDescriptor()). ++>>>>>>> theirs */ - newRel = table_open(newRelId, NoLock); + SetIndexStorageProperties(rel, attrel, attnum, + false, 0, + true, cmethod, + lockmode); - /* Find or create a work queue entry for the newly created table. */ - new_partrel_tab = ATGetQueueEntry(wqueue, newRel); + heap_freetuple(tuple); - /* Create constraints, default values, and generated values. */ - createTableConstraints(wqueue, new_partrel_tab, parent_rel, newRel); + table_close(attrel, RowExclusiveLock); - /* - * Need to call CommandCounterIncrement, so a fresh relcache entry has - * newly installed constraint info. - */ + /* make changes visible */ CommandCounterIncrement(); - return newRel; + ObjectAddressSubSet(address, RelationRelationId, + RelationGetRelid(rel), attnum); + return address; } + /* - * MergePartitionsMoveRows: scan partitions to be merged (mergingPartitions) - * of the partitioned table and move rows into the new partition - * (newPartRel). We also verify check constraints against these rows. + * Preparation phase for SET LOGGED/UNLOGGED + * + * This verifies that we're not trying to change a temp table. Also, + * existing foreign key constraints are checked to avoid ending up with + * permanent tables referencing unlogged tables. */ static void - MergePartitionsMoveRows(List **wqueue, List *mergingPartitions, Relation newPartRel) + ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged) { ++<<<<<<< ours + CommandId mycid; + EState *estate; + AlteredTableInfo *tab; + ListCell *ltab; + + /* The FSM is empty, so don't bother using it. */ + uint32 ti_options = TABLE_INSERT_SKIP_FSM; + BulkInsertState bistate; /* state of bulk inserts for partition */ + TupleTableSlot *dstslot; + + /* Find the work queue entry for the new partition table: newPartRel. */ + tab = ATGetQueueEntry(wqueue, newPartRel); + + /* Generate the constraint and default execution states. */ + estate = CreateExecutorState(); + + buildExpressionExecutionStates(tab, newPartRel, estate); + + mycid = GetCurrentCommandId(true); + + /* Prepare a BulkInsertState for table_tuple_insert. */ + bistate = GetBulkInsertState(); + + /* Create the necessary tuple slot. */ + dstslot = table_slot_create(newPartRel, NULL); ++======= + Relation pg_constraint; + HeapTuple tuple; + SysScanDesc scan; + ScanKeyData skey[1]; ++>>>>>>> theirs - foreach_oid(merging_oid, mergingPartitions) + /* + * Disallow changing status for a temp table. Also verify whether we can + * get away with doing nothing; in such cases we don't need to run the + * checks below, either. + */ + switch (rel->rd_rel->relpersistence) { - ExprContext *econtext; - TupleTableSlot *srcslot; - TupleConversionMap *tuple_map; - TableScanDesc scan; - MemoryContext oldCxt; - Snapshot snapshot; - Relation mergingPartition; + case RELPERSISTENCE_TEMP: + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("cannot change logged status of table \"%s\" because it is temporary", + RelationGetRelationName(rel)), + errtable(rel))); + break; + case RELPERSISTENCE_PERMANENT: + if (toLogged) + /* nothing to do */ + return; + break; + case RELPERSISTENCE_UNLOGGED: + if (!toLogged) + /* nothing to do */ + return; + break; + } - econtext = GetPerTupleExprContext(estate); + /* + * Check that the table is not part of any publication when changing to + * UNLOGGED, as UNLOGGED tables can't be published. + */ + if (!toLogged && + GetRelationPublications(RelationGetRelid(rel)) != NIL) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot change table \"%s\" to unlogged because it is part of a publication", + RelationGetRelationName(rel)), + errdetail("Unlogged relations cannot be replicated."))); - /* - * Partition is already locked in the transformPartitionCmdForMerge - * function. - */ - mergingPartition = table_open(merging_oid, NoLock); + /* + * Check existing foreign key constraints to preserve the invariant that + * permanent tables cannot reference unlogged ones. Self-referencing + * foreign keys can safely be ignored. + */ + pg_constraint = table_open(ConstraintRelationId, AccessShareLock); - /* Create a source tuple slot for the partition being merged. */ - srcslot = table_slot_create(mergingPartition, NULL); + /* + * Scan conrelid if changing to permanent, else confrelid. This also + * determines whether a useful index exists. + */ + ScanKeyInit(&skey[0], + toLogged ? Anum_pg_constraint_conrelid : + Anum_pg_constraint_confrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(rel))); + scan = systable_beginscan(pg_constraint, + toLogged ? ConstraintRelidTypidNameIndexId : InvalidOid, + true, NULL, 1, skey); - /* - * Map computing for moving attributes of the merged partition to the - * new partition. - */ - tuple_map = convert_tuples_by_name(RelationGetDescr(mergingPartition), - RelationGetDescr(newPartRel)); + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(tuple); ++<<<<<<< ours + /* Scan through the rows. */ + snapshot = RegisterSnapshot(GetLatestSnapshot()); + scan = table_beginscan(mergingPartition, snapshot, 0, NULL, + SO_NONE); ++======= + if (con->contype == CONSTRAINT_FOREIGN) + { + Oid foreignrelid; + Relation foreignrel; ++>>>>>>> theirs - /* - * Switch to per-tuple memory context and reset it for each tuple - * produced, so we don't leak memory. - */ - oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate)); + /* the opposite end of what we used as scankey */ + foreignrelid = toLogged ? con->confrelid : con->conrelid; - while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot)) - { - TupleTableSlot *insertslot; + /* ignore if self-referencing */ + if (RelationGetRelid(rel) == foreignrelid) + continue; - CHECK_FOR_INTERRUPTS(); + foreignrel = relation_open(foreignrelid, AccessShareLock); - if (tuple_map) + if (toLogged) { - /* Need to use a map to copy attributes. */ - insertslot = execute_attr_map_slot(tuple_map->attrMap, srcslot, dstslot); + if (!RelationIsPermanent(foreignrel)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("could not change table \"%s\" to logged because it references unlogged table \"%s\"", + RelationGetRelationName(rel), + RelationGetRelationName(foreignrel)), + errtableconstraint(rel, NameStr(con->conname)))); } else { @@@ -23532,637 -17833,430 +22034,763 @@@ AlterTableNamespaceInternal(Relation re } /* ++<<<<<<< ours + * equal_oid_lists: return true if two OID lists, each sorted in ascending + * order, contain the same OIDs in the same order. + */ +static bool +equal_oid_lists(const List *a, const List *b) +{ + ListCell *la, + *lb; + + if (list_length(a) != list_length(b)) + return false; + + forboth(la, a, lb, b) + { + if (lfirst_oid(la) != lfirst_oid(lb)) + return false; + } + return true; +} + +/* + * Comparator for list_sort() on a list of PartitionIndexExtDepEntry *. + * Orders by parentIndexOid, then by indexOid as a tiebreaker so conflict + * reports for different parent indexes are deterministic. + */ +static int +cmp_partition_index_ext_dep(const ListCell *a, const ListCell *b) +{ + const PartitionIndexExtDepEntry *ea = lfirst(a); + const PartitionIndexExtDepEntry *eb = lfirst(b); + + if (ea->parentIndexOid != eb->parentIndexOid) + return pg_cmp_u32(ea->parentIndexOid, eb->parentIndexOid); + return pg_cmp_u32(ea->indexOid, eb->indexOid); +} + +/* + * collectPartitionIndexExtDeps: collect extension dependencies from indexes + * on the given partitions. + * + * For each partition index that has a parent partitioned index, we collect + * extension dependencies. All source partition indexes sharing the same + * parent partitioned index must depend on exactly the same set of + * extensions; otherwise an error is raised so that we neither silently drop + * nor silently add dependencies on the merged partition's index. + * + * Indexes that don't have a parent partitioned index (i.e., indexes created + * directly on a partition without a corresponding parent index) are skipped. + * + * The returned list is sorted by parentIndexOid with exactly one entry per + * parent partitioned index, so applyPartitionIndexExtDeps() can scan it + * linearly. + */ +static List * +collectPartitionIndexExtDeps(List *partitionOids) +{ + List *collected = NIL; + List *result = NIL; + PartitionIndexExtDepEntry *prev = NULL; + + /* + * Phase 1: collect one entry per (partition index -> parent index) pair, + * with its extension dependency OIDs sorted ascending. + */ + foreach_oid(partOid, partitionOids) + { + Relation partRel; + List *indexList; + + /* + * Use NoLock since the caller already holds AccessExclusiveLock on + * these partitions. + */ + partRel = table_open(partOid, NoLock); + indexList = RelationGetIndexList(partRel); + + foreach_oid(indexOid, indexList) + { + Oid parentIndexOid; + PartitionIndexExtDepEntry *entry; + + if (!get_rel_relispartition(indexOid)) + continue; + + parentIndexOid = get_partition_parent(indexOid, true); + if (!OidIsValid(parentIndexOid)) + continue; + + entry = palloc(sizeof(PartitionIndexExtDepEntry)); + entry->parentIndexOid = parentIndexOid; + entry->indexOid = indexOid; + entry->extensionOids = getAutoExtensionsOfObject(RelationRelationId, + indexOid); + list_sort(entry->extensionOids, list_oid_cmp); + + collected = lappend(collected, entry); + } + + list_free(indexList); + table_close(partRel, NoLock); + } + + /* + * Phase 2: sort by parentIndexOid so entries sharing a parent index sit + * adjacent. + */ + list_sort(collected, cmp_partition_index_ext_dep); + + /* + * Phase 3: single linear pass verifying that adjacent entries sharing a + * parent index have identical extension dependencies, and keeping one + * representative entry per parent index. + */ + foreach_ptr(PartitionIndexExtDepEntry, entry, collected) + { + if (prev != NULL && prev->parentIndexOid == entry->parentIndexOid) + { + if (!equal_oid_lists(prev->extensionOids, entry->extensionOids)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot merge partitions with conflicting extension dependencies"), + errdetail("Partition indexes \"%s\" and \"%s\" depend on different extensions.", + get_rel_name(prev->indexOid), + get_rel_name(entry->indexOid)))); + + /* Duplicate entry for the same parent index; discard. */ + list_free(entry->extensionOids); + pfree(entry); + continue; + } + + result = lappend(result, entry); + prev = entry; + } + + list_free(collected); + + return result; +} + +/* + * applyPartitionIndexExtDeps: apply collected extension dependencies to + * indexes on a new partition. + * + * For each index on the new partition, look up its parent index in the + * extDepState list. If found, record extension dependencies on the new index. + * extDepState is sorted by parentIndexOid, so the inner scan can bail out + * as soon as it passes the target OID. + */ +static void +applyPartitionIndexExtDeps(Oid newPartOid, List *extDepState) +{ + Relation partRel; + List *indexList; + + if (extDepState == NIL) + return; + + /* + * Use NoLock since the caller already holds AccessExclusiveLock on the + * new partition. + */ + partRel = table_open(newPartOid, NoLock); + indexList = RelationGetIndexList(partRel); + + foreach_oid(indexOid, indexList) + { + Oid parentIdxOid; + + if (!get_rel_relispartition(indexOid)) + continue; + + parentIdxOid = get_partition_parent(indexOid, true); + if (!OidIsValid(parentIdxOid)) + continue; + + foreach_ptr(PartitionIndexExtDepEntry, entry, extDepState) + { + ObjectAddress indexAddr; + + if (entry->parentIndexOid > parentIdxOid) + break; + if (entry->parentIndexOid < parentIdxOid) + continue; + + ObjectAddressSet(indexAddr, RelationRelationId, indexOid); + + foreach_oid(extOid, entry->extensionOids) + { + ObjectAddress extAddr; + + ObjectAddressSet(extAddr, ExtensionRelationId, extOid); + recordDependencyOn(&indexAddr, &extAddr, + DEPENDENCY_AUTO_EXTENSION); + } + break; + } + } + + list_free(indexList); + table_close(partRel, NoLock); +} + +/* + * freePartitionIndexExtDeps: free memory allocated by collectPartitionIndexExtDeps. + */ +static void +freePartitionIndexExtDeps(List *extDepState) +{ + foreach_ptr(PartitionIndexExtDepEntry, entry, extDepState) + { + list_free(entry->extensionOids); + pfree(entry); + } + list_free(extDepState); +} + +/* + * ALTER TABLE MERGE PARTITIONS INTO ++======= + * The guts of relocating a relation to another namespace: fix the pg_class + * entry, and the pg_depend entry if any. Caller must already have + * opened and write-locked pg_class. ++>>>>>>> theirs */ - static void - ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel, - PartitionCmd *cmd, AlterTableUtilityContext *context) + void + AlterRelationNamespaceInternal(Relation classRel, Oid relOid, + Oid oldNspOid, Oid newNspOid, + bool hasDependEntry, + ObjectAddresses *objsMoved) { ++<<<<<<< ours + Relation newPartRel; + List *mergingPartitions = NIL; + List *extDepState = NIL; + Oid defaultPartOid; + Oid existingRelid; + Oid ownerId = InvalidOid; + Oid save_userid; + int save_sec_context; + int save_nestlevel; + + /* + * Check ownership of merged partitions - partitions with different owners + * cannot be merged. Also, collect the OIDs of these partitions during the + * check. + */ + foreach_node(RangeVar, name, cmd->partlist) + { + Relation mergingPartition; + + /* + * We are going to detach and remove this partition. We already took + * AccessExclusiveLock lock on transformPartitionCmdForMerge, so here, + * NoLock is fine. + */ + mergingPartition = table_openrv_extended(name, NoLock, false); + Assert(CheckRelationLockedByMe(mergingPartition, AccessExclusiveLock, false)); + + if (OidIsValid(ownerId)) + { + /* Do the partitions being merged have different owners? */ + if (ownerId != mergingPartition->rd_rel->relowner) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("partitions being merged have different owners")); + } + else + ownerId = mergingPartition->rd_rel->relowner; ++======= + HeapTuple classTup; + Form_pg_class classForm; + ObjectAddress thisobj; + bool already_done = false; ++>>>>>>> theirs - /* Store the next merging partition into the list. */ - mergingPartitions = lappend_oid(mergingPartitions, - RelationGetRelid(mergingPartition)); + /* 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); - table_close(mergingPartition, NoLock); - } + Assert(classForm->relnamespace == oldNspOid); - /* Look up the existing relation by the new partition name. */ - RangeVarGetAndCheckCreationNamespace(cmd->name, NoLock, &existingRelid); + thisobj.classId = RelationRelationId; + thisobj.objectId = relOid; + thisobj.objectSubId = 0; /* - * Check if this name is already taken. This helps us to detect the - * situation when one of the merging partitions has the same name as the - * new partition. Otherwise, this would fail later on anyway, but - * catching this here allows us to emit a nicer error message. + * If the object has already been moved, don't move it again. If it's + * already in the right place, don't move it, but still fire the object + * access hook. */ - if (OidIsValid(existingRelid)) + already_done = object_address_present(&thisobj, objsMoved); + if (!already_done && oldNspOid != newNspOid) { - if (list_member_oid(mergingPartitions, existingRelid)) - { - /* - * The new partition has the same name as one of the merging - * partitions. - */ - char tmpRelName[NAMEDATALEN]; + ItemPointerData otid = classTup->t_self; - /* Generate a temporary name. */ - sprintf(tmpRelName, "merge-%u-%X-tmp", RelationGetRelid(rel), MyProcPid); + /* check for duplicate name (more friendly than unique-index failure) */ + if (get_relname_relid(NameStr(classForm->relname), + newNspOid) != InvalidOid) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("relation \"%s\" already exists in schema \"%s\"", + NameStr(classForm->relname), + get_namespace_name(newNspOid)))); - /* - * Rename the existing partition with a temporary name, leaving it - * free for the new partition. We don't need to care about this - * in the future because we're going to eventually drop the - * existing partition anyway. - */ - RenameRelationInternal(existingRelid, tmpRelName, true, false); + /* classTup is a copy, so OK to scribble on */ + classForm->relnamespace = newNspOid; - /* - * We must bump the command counter to make the new partition - * tuple visible for rename. - */ - CommandCounterIncrement(); - } - else - { - ereport(ERROR, - errcode(ERRCODE_DUPLICATE_TABLE), - errmsg("relation \"%s\" already exists", cmd->name->relname)); - } - } + CatalogTupleUpdate(classRel, &otid, classTup); + UnlockTuple(classRel, &otid, InplaceUpdateTupleLock); - defaultPartOid = - get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true)); ++<<<<<<< ours + /* + * Collect extension dependencies from indexes on the merging partitions. + * We must do this before detaching them, so we can restore the + * dependencies on the new partition's indexes later. + */ + extDepState = collectPartitionIndexExtDeps(mergingPartitions); + + /* Detach all merging partitions. */ + foreach_oid(mergingPartitionOid, mergingPartitions) ++======= + /* Update dependency on schema if caller said so */ + if (hasDependEntry && + changeDependencyFor(RelationRelationId, + relOid, + NamespaceRelationId, + oldNspOid, + newNspOid) != 1) + elog(ERROR, "could not change schema dependency for relation \"%s\"", + NameStr(classForm->relname)); + } + else + UnlockTuple(classRel, &classTup->t_self, InplaceUpdateTupleLock); + if (!already_done) ++>>>>>>> theirs { - Relation child_rel; + add_exact_object_address(&thisobj, objsMoved); - child_rel = table_open(mergingPartitionOid, NoLock); + InvokeObjectPostAlterHook(RelationRelationId, relOid, 0); + } - detachPartitionTable(rel, child_rel, defaultPartOid); + heap_freetuple(classTup); + } - table_close(child_rel, NoLock); - } + /* + * Move all indexes for the specified relation to another namespace. + * + * Note: we assume adequate permission checking was done by the caller, + * and that the caller has a suitable lock on the owning relation. + */ + static void + AlterIndexNamespaces(Relation classRel, Relation rel, + Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved) + { + List *indexList; + ListCell *l; - /* - * Perform a preliminary check to determine whether it's safe to drop all - * merging partitions before we actually do so later. After merging rows - * into the new partitions via MergePartitionsMoveRows, all old partitions - * need to be dropped. However, since the drop behavior is DROP_RESTRICT - * and the merge process (MergePartitionsMoveRows) can be time-consuming, - * performing an early check on the drop eligibility of old partitions is - * preferable. - */ - foreach_oid(mergingPartitionOid, mergingPartitions) + indexList = RelationGetIndexList(rel); + + foreach(l, indexList) { - ObjectAddress object; + Oid indexOid = lfirst_oid(l); + ObjectAddress thisobj; - /* Get oid of the later to be dropped relation. */ - object.objectId = mergingPartitionOid; - object.classId = RelationRelationId; - object.objectSubId = 0; + thisobj.classId = RelationRelationId; + thisobj.objectId = indexOid; + thisobj.objectSubId = 0; ++<<<<<<< ours + performDeletionCheck(&object, DROP_RESTRICT, 0); ++======= + /* + * Note: currently, the index will not have its own dependency on the + * namespace, so we don't need to do changeDependencyFor(). There's no + * row type in pg_type, either. + * + * XXX this objsMoved test may be pointless -- surely we have a single + * dependency link from a relation to each index? + */ + if (!object_address_present(&thisobj, objsMoved)) + { + AlterRelationNamespaceInternal(classRel, indexOid, + oldNspOid, newNspOid, + false, objsMoved); + add_exact_object_address(&thisobj, objsMoved); + } ++>>>>>>> theirs } - /* - * Create a table for the new partition, using the partitioned table as a - * model. - */ - Assert(OidIsValid(ownerId)); - newPartRel = createPartitionTable(wqueue, cmd->name, rel, ownerId); + list_free(indexList); + } + + /* + * Move all identity and SERIAL-column sequences of the specified relation to another + * namespace. + * + * Note: we assume adequate permission checking was done by the caller, + * and that the caller has a suitable lock on the owning relation. + */ + static void + AlterSeqNamespaces(Relation classRel, Relation rel, + Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved, + LOCKMODE lockmode) + { + Relation depRel; + SysScanDesc scan; + ScanKeyData key[2]; + HeapTuple tup; /* - * Switch to the table owner's userid, so that any index functions are run - * as that user. Also, lockdown security-restricted operations and - * arrange to make GUC variable changes local to this command. - * - * Need to do it after determining the namespace in the - * createPartitionTable() call. + * SERIAL sequences are those having an auto dependency on one of the + * table's columns (we don't care *which* column, exactly). */ - GetUserIdAndSecContext(&save_userid, &save_sec_context); - SetUserIdAndSecContext(ownerId, - save_sec_context | SECURITY_RESTRICTED_OPERATION); - save_nestlevel = NewGUCNestLevel(); - RestrictSearchPath(); + depRel = table_open(DependRelationId, AccessShareLock); + + ScanKeyInit(&key[0], + Anum_pg_depend_refclassid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationRelationId)); + ScanKeyInit(&key[1], + Anum_pg_depend_refobjid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(rel))); + /* we leave refobjsubid unspecified */ - /* Copy data from merged partitions to the new partition. */ - MergePartitionsMoveRows(wqueue, mergingPartitions, newPartRel); + scan = systable_beginscan(depRel, DependReferenceIndexId, true, + NULL, 2, key); - /* Drop the current partitions before attaching the new one. */ - foreach_oid(mergingPartitionOid, mergingPartitions) + while (HeapTupleIsValid(tup = systable_getnext(scan))) { - ObjectAddress object; + Form_pg_depend depForm = (Form_pg_depend) GETSTRUCT(tup); + Relation seqRel; - object.objectId = mergingPartitionOid; - object.classId = RelationRelationId; - object.objectSubId = 0; + /* skip dependencies other than auto dependencies on columns */ + if (depForm->refobjsubid == 0 || + depForm->classid != RelationRelationId || + depForm->objsubid != 0 || + !(depForm->deptype == DEPENDENCY_AUTO || depForm->deptype == DEPENDENCY_INTERNAL)) + continue; - performDeletion(&object, DROP_RESTRICT, 0); - } + /* Use relation_open just in case it's an index */ + seqRel = relation_open(depForm->objid, lockmode); - list_free(mergingPartitions); + /* skip non-sequence relations */ + if (RelationGetForm(seqRel)->relkind != RELKIND_SEQUENCE) + { + /* No need to keep the lock */ + relation_close(seqRel, lockmode); + continue; + } - /* - * Attach a new partition to the partitioned table. wqueue = NULL: - * verification for each cloned constraint is not needed. - */ - attachPartitionTable(NULL, rel, newPartRel, cmd->bound); + /* Fix the pg_class and pg_depend entries */ + AlterRelationNamespaceInternal(classRel, depForm->objid, + oldNspOid, newNspOid, + true, objsMoved); + /* + * Sequences used to have entries in pg_type, but no longer do. If we + * ever re-instate that, we'll need to move the pg_type entry to the + * new namespace, too (using AlterTypeNamespaceInternal). + */ + Assert(RelationGetForm(seqRel)->reltype == InvalidOid); + ++<<<<<<< ours + /* + * Apply extension dependencies to the new partition's indexes. This + * preserves any "DEPENDS ON EXTENSION" settings from the merged + * partitions. + */ + applyPartitionIndexExtDeps(RelationGetRelid(newPartRel), extDepState); + + freePartitionIndexExtDeps(extDepState); + + /* Keep the lock until commit. */ + table_close(newPartRel, NoLock); ++======= + /* Now we can close it. Keep the lock till end of transaction. */ + relation_close(seqRel, NoLock); + } ++>>>>>>> theirs - /* Roll back any GUC changes executed by index functions. */ - AtEOXact_GUC(false, save_nestlevel); + systable_endscan(scan); - /* Restore the userid and security context. */ - SetUserIdAndSecContext(save_userid, save_sec_context); + relation_close(depRel, AccessShareLock); } + /* - * Struct with the context of the new partition for inserting rows from the - * split partition. + * This code supports + * CREATE TEMP TABLE ... ON COMMIT { DROP | PRESERVE ROWS | DELETE ROWS } + * + * Because we only support this for TEMP tables, it's sufficient to remember + * the state in a backend-local data structure. */ - typedef struct SplitPartitionContext - { - ExprState *partqualstate; /* expression for checking a slot for a - * partition (NULL for DEFAULT partition) */ - BulkInsertState bistate; /* state of bulk inserts for partition */ - TupleTableSlot *dstslot; /* slot for inserting row into partition */ - AlteredTableInfo *tab; /* structure with generated column expressions - * and check constraint expressions. */ - Relation partRel; /* relation for partition */ - } SplitPartitionContext; /* - * createSplitPartitionContext: create context for partition and fill it + * Register a newly-created relation's ON COMMIT action. */ - static SplitPartitionContext * - createSplitPartitionContext(Relation partRel) + void + register_on_commit_action(Oid relid, OnCommitAction action) { - SplitPartitionContext *pc; - - pc = palloc0_object(SplitPartitionContext); - pc->partRel = partRel; + OnCommitItem *oc; + MemoryContext oldcxt; /* - * Prepare a BulkInsertState for table_tuple_insert. The FSM is empty, so - * don't bother using it. + * We needn't bother registering the relation unless there is an ON COMMIT + * action we need to take. */ - pc->bistate = GetBulkInsertState(); + if (action == ONCOMMIT_NOOP || action == ONCOMMIT_PRESERVE_ROWS) + return; + + oldcxt = MemoryContextSwitchTo(CacheMemoryContext); - /* Create a destination tuple slot for the new partition. */ - pc->dstslot = table_slot_create(pc->partRel, NULL); + oc = palloc_object(OnCommitItem); + oc->relid = relid; + oc->oncommit = action; + oc->creating_subid = GetCurrentSubTransactionId(); + oc->deleting_subid = InvalidSubTransactionId; + + /* + * We use lcons() here so that ON COMMIT actions are processed in reverse + * order of registration. That might not be essential but it seems + * reasonable. + */ + on_commits = lcons(oc, on_commits); - return pc; + MemoryContextSwitchTo(oldcxt); } /* - * deleteSplitPartitionContext: delete context for partition + * Unregister any ON COMMIT action when a relation is deleted. + * + * Actually, we only mark the OnCommitItem entry as to be deleted after commit. */ ++<<<<<<< ours +static void +deleteSplitPartitionContext(SplitPartitionContext *pc, List **wqueue, uint32 ti_options) ++======= + void + remove_on_commit_action(Oid relid) ++>>>>>>> theirs { - ListCell *ltab; - - ExecDropSingleTupleTableSlot(pc->dstslot); - FreeBulkInsertState(pc->bistate); - - table_finish_bulk_insert(pc->partRel, ti_options); + ListCell *l; - /* - * We don't need to process this pc->partRel so delete the ALTER TABLE - * queue of it. - */ - foreach(ltab, *wqueue) + foreach(l, on_commits) { - AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab); + OnCommitItem *oc = (OnCommitItem *) lfirst(l); - if (tab->relid == RelationGetRelid(pc->partRel)) + if (oc->relid == relid) { - *wqueue = list_delete_cell(*wqueue, ltab); + oc->deleting_subid = GetCurrentSubTransactionId(); break; } } } /* - * SplitPartitionMoveRows: scan split partition (splitRel) of partitioned table - * (rel) and move rows into new partitions. - * - * New partitions description: - * partlist: list of pointers to SinglePartitionSpec structures. It contains - * the partition specification details for all new partitions. - * newPartRels: list of Relations, new partitions created in - * ATExecSplitPartition. + * Perform ON COMMIT actions. + * + * This is invoked just before actually committing, since it's possible + * to encounter errors. */ - static void - SplitPartitionMoveRows(List **wqueue, Relation rel, Relation splitRel, - List *partlist, List *newPartRels) + void + PreCommit_on_commit_actions(void) { ++<<<<<<< ours + /* The FSM is empty, so don't bother using it. */ + uint32 ti_options = TABLE_INSERT_SKIP_FSM; + CommandId mycid; + EState *estate; + ListCell *listptr, + *listptr2; + TupleTableSlot *srcslot; + ExprContext *econtext; + TableScanDesc scan; + Snapshot snapshot; + MemoryContext oldCxt; + List *partContexts = NIL; + TupleConversionMap *tuple_map; + SplitPartitionContext *defaultPartCtx = NULL, + *pc; ++======= + ListCell *l; + List *oids_to_truncate = NIL; + List *oids_to_drop = NIL; ++>>>>>>> theirs - mycid = GetCurrentCommandId(true); + foreach(l, on_commits) + { + OnCommitItem *oc = (OnCommitItem *) lfirst(l); - estate = CreateExecutorState(); + /* Ignore entry if already dropped in this xact */ + if (oc->deleting_subid != InvalidSubTransactionId) + continue; - forboth(listptr, partlist, listptr2, newPartRels) - { - SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr); + switch (oc->oncommit) + { + case ONCOMMIT_NOOP: + case ONCOMMIT_PRESERVE_ROWS: + /* Do nothing (there shouldn't be such entries, actually) */ + break; + case ONCOMMIT_DELETE_ROWS: - pc = createSplitPartitionContext((Relation) lfirst(listptr2)); + /* + * If this transaction hasn't accessed any temporary + * relations, we can skip truncating ON COMMIT DELETE ROWS + * tables, as they must still be empty. + */ + if ((MyXactFlags & XACT_FLAGS_ACCESSEDTEMPNAMESPACE)) + oids_to_truncate = lappend_oid(oids_to_truncate, oc->relid); + break; + case ONCOMMIT_DROP: + oids_to_drop = lappend_oid(oids_to_drop, oc->relid); + break; + } + } - /* Find the work queue entry for the new partition table: newPartRel. */ - pc->tab = ATGetQueueEntry(wqueue, pc->partRel); + /* + * Truncate relations before dropping so that all dependencies between + * relations are removed after they are worked on. Doing it like this + * might be a waste as it is possible that a relation being truncated will + * be dropped anyway due to its parent being dropped, but this makes the + * code more robust because of not having to re-check that the relation + * exists at truncation time. + */ + if (oids_to_truncate != NIL) + heap_truncate(oids_to_truncate); - buildExpressionExecutionStates(pc->tab, pc->partRel, estate); + if (oids_to_drop != NIL) + { + ObjectAddresses *targetObjects = new_object_addresses(); - if (sps->bound->is_default) - { - /* - * We should not create a structure to check the partition - * constraint for the new DEFAULT partition. - */ - defaultPartCtx = pc; - } - else + foreach(l, oids_to_drop) { - List *partConstraint; + ObjectAddress object; - /* Build expression execution states for partition check quals. */ - partConstraint = get_qual_from_partbound(rel, sps->bound); - partConstraint = - (List *) eval_const_expressions(NULL, - (Node *) partConstraint); - /* Make a boolean expression for ExecCheck(). */ - partConstraint = list_make1(make_ands_explicit(partConstraint)); + object.classId = RelationRelationId; + object.objectId = lfirst_oid(l); + object.objectSubId = 0; - /* - * Map the vars in the constraint expression from rel's attnos to - * splitRel's. - */ - partConstraint = map_partition_varattnos(partConstraint, - 1, splitRel, rel); + Assert(!object_address_present(&object, targetObjects)); - pc->partqualstate = - ExecPrepareExpr((Expr *) linitial(partConstraint), estate); - Assert(pc->partqualstate != NULL); + add_exact_object_address(&object, targetObjects); } - /* Store partition context into a list. */ - partContexts = lappend(partContexts, pc); - } + /* + * Object deletion might involve toast table access (to clean up + * toasted catalog entries), so ensure we have a valid snapshot. + */ + PushActiveSnapshot(GetTransactionSnapshot()); - econtext = GetPerTupleExprContext(estate); + /* + * Since this is an automatic drop, rather than one directly initiated + * by the user, we pass the PERFORM_DELETION_INTERNAL flag. + */ + performMultipleDeletions(targetObjects, DROP_CASCADE, + PERFORM_DELETION_INTERNAL | PERFORM_DELETION_QUIETLY); - /* Create the necessary tuple slot. */ - srcslot = table_slot_create(splitRel, NULL); + PopActiveSnapshot(); - /* - * Map computing for moving attributes of the split partition to the new - * partition (for the first new partition, but other new partitions can - * use the same map). - */ - pc = (SplitPartitionContext *) lfirst(list_head(partContexts)); - tuple_map = convert_tuples_by_name(RelationGetDescr(splitRel), - RelationGetDescr(pc->partRel)); + #ifdef USE_ASSERT_CHECKING ++<<<<<<< ours + /* Scan through the rows. */ + snapshot = RegisterSnapshot(GetLatestSnapshot()); + scan = table_beginscan(splitRel, snapshot, 0, NULL, + SO_NONE); ++======= + /* + * Note that table deletion will call remove_on_commit_action, so the + * entry should get marked as deleted. + */ + foreach(l, on_commits) + { + OnCommitItem *oc = (OnCommitItem *) lfirst(l); ++>>>>>>> theirs - /* - * Switch to per-tuple memory context and reset it for each tuple - * produced, so we don't leak memory. - */ - oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate)); + if (oc->oncommit != ONCOMMIT_DROP) + continue; - while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot)) - { - bool found = false; - TupleTableSlot *insertslot; + Assert(oc->deleting_subid != InvalidSubTransactionId); + } + #endif + } + } - CHECK_FOR_INTERRUPTS(); + /* + * Post-commit or post-abort cleanup for ON COMMIT management. + * + * All we do here is remove no-longer-needed OnCommitItem entries. + * + * During commit, remove entries that were deleted during this transaction; + * during abort, remove those created during this transaction. + */ + void + AtEOXact_on_commit_actions(bool isCommit) + { + ListCell *cur_item; - econtext->ecxt_scantuple = srcslot; + foreach(cur_item, on_commits) + { + OnCommitItem *oc = (OnCommitItem *) lfirst(cur_item); - /* Search partition for the current slot, srcslot. */ - foreach(listptr, partContexts) + if (isCommit ? oc->deleting_subid != InvalidSubTransactionId : + oc->creating_subid != InvalidSubTransactionId) { - pc = (SplitPartitionContext *) lfirst(listptr); - - /* skip DEFAULT partition */ - if (pc->partqualstate && ExecCheck(pc->partqualstate, econtext)) - { - found = true; - break; - } + /* cur_item must be removed */ + on_commits = foreach_delete_current(on_commits, cur_item); + pfree(oc); } - if (!found) + else { ++<<<<<<< ours + /* Use the DEFAULT partition if it exists. */ + if (defaultPartCtx) + pc = defaultPartCtx; + else + ereport(ERROR, + errcode(ERRCODE_CHECK_VIOLATION), + errmsg("cannot find partition for split partition row"), + errtable(splitRel)); ++======= + /* cur_item must be preserved */ + oc->creating_subid = InvalidSubTransactionId; + oc->deleting_subid = InvalidSubTransactionId; ++>>>>>>> theirs } + } + } + + /* + * Post-subcommit or post-subabort cleanup for ON COMMIT management. + * + * During subabort, we can immediately remove entries created during this + * subtransaction. During subcommit, just relabel entries marked during + * this subtransaction as being the parent's responsibility. + */ + void + AtEOSubXact_on_commit_actions(bool isCommit, SubTransactionId mySubid, + SubTransactionId parentSubid) + { + ListCell *cur_item; + + foreach(cur_item, on_commits) + { + OnCommitItem *oc = (OnCommitItem *) lfirst(cur_item); - if (tuple_map) + if (!isCommit && oc->creating_subid == mySubid) { - /* Need to use a map to copy attributes. */ - insertslot = execute_attr_map_slot(tuple_map->attrMap, srcslot, pc->dstslot); + /* cur_item must be removed */ + on_commits = foreach_delete_current(on_commits, cur_item); + pfree(oc); } else { @@@ -24222,171 -18366,216 +22900,283 @@@ RangeVarCallbackOwnsRelation(const Rang } /* - * ALTER TABLE SPLIT PARTITION INTO + * Common RangeVarGetRelid callback for rename, set schema, and alter table + * processing. */ static void - ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel, - PartitionCmd *cmd, AlterTableUtilityContext *context) + RangeVarCallbackForAlterRelation(const RangeVar *rv, Oid relid, Oid oldrelid, + void *arg) { ++<<<<<<< ours + Relation splitRel; + Oid splitRelOid; + ListCell *listptr, + *listptr2; + bool isSameName = false; + char tmpRelName[NAMEDATALEN]; + List *newPartRels = NIL; + List *extDepState = NIL; + ObjectAddress object; + Oid defaultPartOid; + Oid save_userid; + int save_sec_context; + int save_nestlevel; + List *splitPartList; ++======= + Node *stmt = (Node *) arg; + ObjectType reltype; + HeapTuple tuple; + Form_pg_class classform; + AclResult aclresult; + char relkind; + + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid)); + if (!HeapTupleIsValid(tuple)) + return; /* concurrently dropped */ + classform = (Form_pg_class) GETSTRUCT(tuple); + relkind = classform->relkind; ++>>>>>>> theirs + + /* Must own relation. */ + if (!object_ownercheck(RelationRelationId, relid, GetUserId())) + aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relid)), rv->relname); - defaultPartOid = get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true)); + /* No system table modifications unless explicitly allowed. */ + if (!allowSystemTableMods && IsSystemClass(relid, classform)) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied: \"%s\" is a system catalog", + rv->relname))); /* - * Partition is already locked in the transformPartitionCmdForSplit - * function. + * Extract the specified relation type from the statement parse tree. + * + * Also, for ALTER .. RENAME, check permissions: the user must (still) + * have CREATE rights on the containing namespace. */ - splitRel = table_openrv(cmd->name, NoLock); - - splitRelOid = RelationGetRelid(splitRel); + if (IsA(stmt, RenameStmt)) + { + aclresult = object_aclcheck(NamespaceRelationId, classform->relnamespace, + GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_SCHEMA, + get_namespace_name(classform->relnamespace)); + reltype = ((RenameStmt *) stmt)->renameType; + } + else if (IsA(stmt, AlterObjectSchemaStmt)) + reltype = ((AlterObjectSchemaStmt *) stmt)->objectType; - /* Check descriptions of new partitions. */ - foreach_node(SinglePartitionSpec, sps, cmd->partlist) + else if (IsA(stmt, AlterTableStmt)) + reltype = ((AlterTableStmt *) stmt)->objtype; + else { - Oid existingRelid; + elog(ERROR, "unrecognized node type: %d", (int) nodeTag(stmt)); + reltype = OBJECT_TABLE; /* placate compiler */ + } + + /* + * For compatibility with prior releases, we allow ALTER TABLE to be used + * with most other types of relations (but not composite types). We allow + * similar flexibility for ALTER INDEX in the case of RENAME, but not + * otherwise. Otherwise, the user must select the correct form of the + * command for the relation at issue. + */ + if (reltype == OBJECT_SEQUENCE && relkind != RELKIND_SEQUENCE) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a sequence", rv->relname))); - /* Look up the existing relation by the new partition name. */ - RangeVarGetAndCheckCreationNamespace(sps->name, NoLock, &existingRelid); + if (reltype == OBJECT_VIEW && relkind != RELKIND_VIEW) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a view", rv->relname))); - /* - * This would fail later on anyway if the relation already exists. But - * by catching it here, we can emit a nicer error message. - */ - if (existingRelid == splitRelOid && !isSameName) - /* One new partition can have the same name as a split partition. */ - isSameName = true; - else if (OidIsValid(existingRelid)) - ereport(ERROR, - errcode(ERRCODE_DUPLICATE_TABLE), - errmsg("relation \"%s\" already exists", sps->name->relname)); - } + if (reltype == OBJECT_MATVIEW && relkind != RELKIND_MATVIEW) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a materialized view", rv->relname))); ++<<<<<<< ours + /* + * Collect extension dependencies from indexes on the split partition. We + * must do this before detaching it, so we can restore the dependencies on + * the new partitions' indexes later. + */ + splitPartList = list_make1_oid(splitRelOid); + + extDepState = collectPartitionIndexExtDeps(splitPartList); + list_free(splitPartList); + + /* Detach the split partition. */ + detachPartitionTable(rel, splitRel, defaultPartOid); ++======= + if (reltype == OBJECT_FOREIGN_TABLE && relkind != RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a foreign table", rv->relname))); + + if (reltype == OBJECT_TYPE && relkind != RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a composite type", rv->relname))); + + if (reltype == OBJECT_INDEX && relkind != RELKIND_INDEX && + relkind != RELKIND_PARTITIONED_INDEX + && !IsA(stmt, RenameStmt)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not an index", rv->relname))); ++>>>>>>> theirs /* - * Perform a preliminary check to determine whether it's safe to drop the - * split partition before we actually do so later. After merging rows into - * the new partitions via SplitPartitionMoveRows, all old partitions need - * to be dropped. However, since the drop behavior is DROP_RESTRICT and - * the merge process (SplitPartitionMoveRows) can be time-consuming, - * performing an early check on the drop eligibility of old partitions is - * preferable. + * Don't allow ALTER TABLE on composite types. We want people to use ALTER + * TYPE for that. */ ++<<<<<<< ours + object.objectId = splitRelOid; + object.classId = RelationRelationId; + object.objectSubId = 0; + performDeletionCheck(&object, DROP_RESTRICT, 0); ++======= + if (reltype != OBJECT_TYPE && relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is a composite type", rv->relname), + /* translator: %s is an SQL ALTER command */ + errhint("Use %s instead.", + "ALTER TYPE"))); ++>>>>>>> theirs /* - * If a new partition has the same name as the split partition, then we - * should rename the split partition to reuse its name. + * Don't allow ALTER TABLE .. SET SCHEMA on relations that can't be moved + * to a different schema, such as indexes and TOAST tables. */ - if (isSameName) + if (IsA(stmt, AlterObjectSchemaStmt)) { - /* - * We must bump the command counter to make the split partition tuple - * visible for renaming. - */ - CommandCounterIncrement(); - /* Rename partition. */ - sprintf(tmpRelName, "split-%u-%X-tmp", RelationGetRelid(rel), MyProcPid); - RenameRelationInternal(splitRelOid, tmpRelName, true, false); - - /* - * We must bump the command counter to make the split partition tuple - * visible after renaming. - */ - CommandCounterIncrement(); + if (relkind == RELKIND_INDEX || relkind == RELKIND_PARTITIONED_INDEX) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot change schema of index \"%s\"", + rv->relname), + errhint("Change the schema of the table instead."))); + else if (relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot change schema of composite type \"%s\"", + rv->relname), + /* translator: %s is an SQL ALTER command */ + errhint("Use %s instead.", + "ALTER TYPE"))); + else if (relkind == RELKIND_TOASTVALUE) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot change schema of TOAST table \"%s\"", + rv->relname), + errhint("Change the schema of the table instead."))); } - /* Create new partitions (like a split partition), without indexes. */ - foreach_node(SinglePartitionSpec, sps, cmd->partlist) - { - Relation newPartRel; + ReleaseSysCache(tuple); + } - newPartRel = createPartitionTable(wqueue, sps->name, rel, - splitRel->rd_rel->relowner); - newPartRels = lappend(newPartRels, newPartRel); - } + /* + * resolve column compression specification to compression method. + */ + static char + GetAttributeCompression(Oid atttypid, const char *compression) + { + char cmethod; + + if (compression == NULL || strcmp(compression, "default") == 0) + return InvalidCompressionMethod; /* - * Switch to the table owner's userid, so that any index functions are run - * as that user. Also, lockdown security-restricted operations and - * arrange to make GUC variable changes local to this command. + * To specify a nondefault method, the column data type must be toastable. + * Note this says nothing about whether the column's attstorage setting + * permits compression; we intentionally allow attstorage and + * attcompression to be independent. But with a non-toastable type, + * attstorage could not be set to a value that would permit compression. * - * Need to do it after determining the namespace in the - * createPartitionTable() call. + * We don't actually need to enforce this, since nothing bad would happen + * if attcompression were non-default; it would never be consulted. But + * it seems more user-friendly to complain about a certainly-useless + * attempt to set the property. */ - GetUserIdAndSecContext(&save_userid, &save_sec_context); - SetUserIdAndSecContext(splitRel->rd_rel->relowner, - save_sec_context | SECURITY_RESTRICTED_OPERATION); - save_nestlevel = NewGUCNestLevel(); - RestrictSearchPath(); + if (!TypeIsToastable(atttypid)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("column data type %s does not support compression", + format_type_be(atttypid)))); - /* Copy data from the split partition to the new partitions. */ - SplitPartitionMoveRows(wqueue, rel, splitRel, cmd->partlist, newPartRels); - /* Keep the lock until commit. */ - table_close(splitRel, NoLock); + cmethod = CompressionNameToMethod(compression); + if (!CompressionMethodIsValid(cmethod)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid compression method \"%s\"", compression))); - /* Attach new partitions to the partitioned table. */ - forboth(listptr, cmd->partlist, listptr2, newPartRels) - { - SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr); - Relation newPartRel = (Relation) lfirst(listptr2); + return cmethod; + } ++<<<<<<< ours + /* + * wqueue = NULL: verification for each cloned constraint is not + * needed. + */ + attachPartitionTable(NULL, rel, newPartRel, sps->bound); + + /* + * Apply extension dependencies to the new partition's indexes. This + * preserves any "DEPENDS ON EXTENSION" settings from the split + * partition. + */ + applyPartitionIndexExtDeps(RelationGetRelid(newPartRel), extDepState); + + /* Keep the lock until commit. */ + table_close(newPartRel, NoLock); + } + + freePartitionIndexExtDeps(extDepState); + + /* Drop the split partition. */ + object.classId = RelationRelationId; + object.objectId = splitRelOid; + object.objectSubId = 0; + /* Probably DROP_CASCADE is not needed. */ + performDeletion(&object, DROP_RESTRICT, 0); ++======= + /* + * resolve column storage specification + */ + static char + GetAttributeStorage(Oid atttypid, const char *storagemode) + { + char cstorage = 0; + + if (pg_strcasecmp(storagemode, "plain") == 0) + cstorage = TYPSTORAGE_PLAIN; + else if (pg_strcasecmp(storagemode, "external") == 0) + cstorage = TYPSTORAGE_EXTERNAL; + else if (pg_strcasecmp(storagemode, "extended") == 0) + cstorage = TYPSTORAGE_EXTENDED; + else if (pg_strcasecmp(storagemode, "main") == 0) + cstorage = TYPSTORAGE_MAIN; + else if (pg_strcasecmp(storagemode, "default") == 0) + cstorage = get_typstorage(atttypid); + else + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid storage type \"%s\"", + storagemode))); ++>>>>>>> theirs - /* Roll back any GUC changes executed by index functions. */ - AtEOXact_GUC(false, save_nestlevel); + /* + * safety check: do not allow toasted storage modes unless column datatype + * is TOAST-aware. + */ + if (!(cstorage == TYPSTORAGE_PLAIN || TypeIsToastable(atttypid))) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("column data type %s can only have storage PLAIN", + format_type_be(atttypid)))); - /* Restore the userid and security context. */ - SetUserIdAndSecContext(save_userid, save_sec_context); + return cstorage; }