=== Applying patches on top of PostgreSQL commit ID 901ed9b352b41f034e17bc540725082a488fce31 === /etc/rc.d/jail: WARNING: Per-jail configuration via jail_* variables is obsolete. Please consider migrating to /etc/jail.conf. Sun May 10 18:11:21 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 343 (offset 38 lines). Hunk #6 succeeded at 365 (offset 38 lines). Hunk #7 succeeded at 385 (offset 38 lines). Hunk #8 succeeded at 414 (offset 38 lines). Hunk #9 succeeded at 422 (offset 38 lines). Hunk #10 succeeded at 467 (offset 38 lines). Hunk #11 succeeded at 477 (offset 38 lines). Hunk #12 FAILED at 515. Hunk #13 succeeded at 4310 (offset 59 lines). Hunk #14 succeeded at 6452 (offset 61 lines). Hunk #15 succeeded at 6633 (offset 61 lines). Hunk #16 succeeded at 6748 (offset 61 lines). Hunk #17 succeeded at 8377 (offset 86 lines). Hunk #18 succeeded at 10599 (offset 73 lines). Hunk #19 succeeded at 10777 (offset 73 lines). Hunk #20 succeeded at 10915 (offset 73 lines). Hunk #21 succeeded at 11081 (offset 73 lines). Hunk #22 succeeded at 11091 (offset 73 lines). Hunk #23 succeeded at 11154 (offset 73 lines). Hunk #24 succeeded at 12133 (offset 242 lines). Hunk #25 succeeded at 16506 (offset 241 lines). Hunk #26 succeeded at 16987 (offset 236 lines). Hunk #27 succeeded at 17005 (offset 236 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 88451c91448,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. @@@ -748,45 -518,6 +556,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. @@@ -12288,596 -11470,288 +11552,592 @@@ 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))); ++======= + 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, + 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); + } + + table_close(rel, NoLock); + + return changed; +} + +/* + * 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, LOCKMODE lockmode) +{ + Form_pg_constraint currcon; + Relation rel; + bool changed = false; + List *children = NIL; + + /* 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); + + if (currcon->conenforced != cmdcon->is_enforced) + { + AlterConstrUpdateConstraintEntry(cmdcon, 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. + * 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) + children = find_all_inheritors(RelationGetRelid(rel), + lockmode, NULL); + + 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.")); + + AlterCheckConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, + childoid, false, true, + 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 && + cmdcon->is_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); ++======= + 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 + * Invokes ATExecAlterCheckConstrEnforceability for each CHECK constraint that + * is a child of the specified constraint. + * + * We rely on the parent and child tables having identical CHECK constraint + * names to retrieve the child's pg_constraint tuple. + * + * The arguments to this function have the same meaning as the arguments to + * ATExecAlterCheckConstrEnforceability. + */ +static void +AlterCheckConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, + Relation conrel, Oid conrelid, + bool recurse, bool recursing, + LOCKMODE lockmode) +{ + SysScanDesc pscan; + HeapTuple childtup; + ScanKeyData skey[3]; + + ScanKeyInit(&skey[0], + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(conrelid)); + 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)); + + pscan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId, true, + NULL, 3, skey); + + if (!HeapTupleIsValid(childtup = systable_getnext(pscan))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("constraint \"%s\" of relation \"%s\" does not exist", + cmdcon->conname, get_rel_name(conrelid))); + + ATExecAlterCheckConstrEnforceability(wqueue, cmdcon, conrel, childtup, + recurse, recursing, lockmode); + + systable_endscan(pscan); +} + +/* + * Returns true if the constraint's deferrability is altered. + * + * *otherrelids is appended OIDs of relations containing affected triggers. + * + * Note that we must recurse even when the values are correct, in case + * indirect descendants have had their constraints altered locally. + * (This could be avoided if we forbade altering constraints in partitions + * but existing releases don't do that.) ++======= + * Update the constraint entry for the given ATAlterConstraint command, and + * invoke the appropriate hooks. ++>>>>>>> theirs */ - static bool - ATExecAlterConstrDeferrability(List **wqueue, ATAlterConstraint *cmdcon, - Relation conrel, Relation tgrel, Relation rel, - HeapTuple contuple, bool recurse, - List **otherrelids, LOCKMODE lockmode) + static void + AlterConstrUpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation conrel, + HeapTuple contuple) { - Form_pg_constraint currcon; - Oid refrelid; - bool changed = false; - - /* since this function recurses, it could be driven to stack overflow */ - check_stack_depth(); - - Assert(cmdcon->alterDeferrability); + HeapTuple copyTuple; + Form_pg_constraint copy_con; - currcon = (Form_pg_constraint) GETSTRUCT(contuple); - refrelid = currcon->confrelid; + Assert(cmdcon->alterEnforceability || cmdcon->alterDeferrability || + cmdcon->alterInheritability); - /* Should be foreign key constraint */ - Assert(currcon->contype == CONSTRAINT_FOREIGN); + copyTuple = heap_copytuple(contuple); + copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); - /* - * If called to modify a constraint that's already in the desired state, - * silently do nothing. - */ - if (currcon->condeferrable != cmdcon->deferrable || - currcon->condeferred != cmdcon->initdeferred) + if (cmdcon->alterEnforceability) { - AlterConstrUpdateConstraintEntry(cmdcon, conrel, contuple); - changed = true; + copy_con->conenforced = cmdcon->is_enforced; /* - * Now we need to update the multiple entries in pg_trigger that - * implement the constraint. + * 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. */ - AlterConstrTriggerDeferrability(currcon->oid, tgrel, rel, - cmdcon->deferrable, - cmdcon->initdeferred, otherrelids); + copy_con->convalidated = cmdcon->is_enforced; + } + if (cmdcon->alterDeferrability) + { + copy_con->condeferrable = cmdcon->deferrable; + copy_con->condeferred = cmdcon->initdeferred; } + if (cmdcon->alterInheritability) + copy_con->connoinherit = cmdcon->noinherit; - /* - * If the table at either end of the constraint is partitioned, we need to - * handle every constraint that is a child of this one. - */ - if (recurse && changed && - (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE || - get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)) - AlterConstrDeferrabilityRecurse(wqueue, cmdcon, conrel, tgrel, rel, - contuple, recurse, otherrelids, - lockmode); + CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple); + InvokeObjectPostAlterHook(ConstraintRelationId, copy_con->oid, 0); - return changed; + /* Make new constraint flags visible to others */ + CacheInvalidateRelcacheByRelid(copy_con->conrelid); + + heap_freetuple(copyTuple); } /* @@@ -12956,117 -11849,122 +12235,161 @@@ ATExecValidateConstraint(List **wqueue } /* - * A subroutine of ATExecAlterConstrDeferrability that updated constraint - * trigger's deferrability. + * QueueFKConstraintValidation * - * The arguments to this function have the same meaning as the arguments to - * ATExecAlterConstrDeferrability. + * 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 - AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel, - bool deferrable, bool initdeferred, - List **otherrelids) + void + QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation fkrel, + Oid pkrelid, HeapTuple contuple, LOCKMODE lockmode) { - HeapTuple tgtuple; - ScanKeyData tgkey; - SysScanDesc tgscan; + Form_pg_constraint con; + AlteredTableInfo *tab; + HeapTuple copyTuple; + Form_pg_constraint copy_con; - 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))) + con = (Form_pg_constraint) GETSTRUCT(contuple); + Assert(con->contype == CONSTRAINT_FOREIGN); + Assert(!con->convalidated); + + /* + * 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) { - Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple); - Form_pg_trigger copy_tg; - HeapTuple tgCopyTuple; + NewConstraint *newcon; + Constraint *fkconstraint; - /* - * 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); + /* Queue validation for phase 3 */ + fkconstraint = makeNode(Constraint); + /* for now this is all we need */ + fkconstraint->conname = pstrdup(NameStr(con->conname)); - /* - * 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; + 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; - tgCopyTuple = heap_copytuple(tgtuple); - copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple); + /* Find or create work queue entry for this table */ + tab = ATGetQueueEntry(wqueue, fkrel); + tab->constraints = lappend(tab->constraints, newcon); + } - copy_tg->tgdeferrable = deferrable; - copy_tg->tginitdeferred = initdeferred; - CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple); + /* + * 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) + { + ScanKeyData pkey; + SysScanDesc pscan; + HeapTuple childtup; - InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0); + ScanKeyInit(&pkey, + Anum_pg_constraint_conparentid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(con->oid)); - heap_freetuple(tgCopyTuple); - } + pscan = systable_beginscan(conrel, ConstraintParentIndexId, + true, NULL, 1, &pkey); - systable_endscan(tgscan); - } + while (HeapTupleIsValid(childtup = systable_getnext(pscan))) + { + Form_pg_constraint childcon; + Relation childrel; ++<<<<<<< ours +/* + * 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 + * ATExecAlterFKConstrEnforceability. + */ +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) +{ + 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; + /* + * 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; - ScanKeyInit(&pkey, - Anum_pg_constraint_conparentid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(conoid)); + childrel = table_open(childcon->conrelid, lockmode); - pscan = systable_beginscan(conrel, ConstraintParentIndexId, - true, NULL, 1, &pkey); + /* + * 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); + } ++<<<<<<< ours + while (HeapTupleIsValid(childtup = systable_getnext(pscan))) + ATExecAlterFKConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid, + pkrelid, childtup, lockmode, + ReferencedParentDelTrigger, + ReferencedParentUpdTrigger, + ReferencingParentInsTrigger, + ReferencingParentUpdTrigger); ++======= + systable_endscan(pscan); + } ++>>>>>>> theirs - systable_endscan(pscan); + /* + * 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. + */ + 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); } /* @@@ -17082,6 -15670,6 +16096,7 @@@ ATExecSetRelOptions(Relation rel, List if (isnull) datum = (Datum) 0; } ++<<<<<<< ours newOptions = transformRelOptions(datum, defList, "toast", validnsps, false, operation == AT_ResetRelOptions); @@@ -17518,3256 -16108,94 +16533,2944 @@@ ATPrepChangeInherit(Relation child_rel 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; + + /* + * 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 child -- child was checked by + * ATSimplePermissions call in ATPrepCmd + */ + ATSimplePermissions(AT_AddInherit, parent_rel, + ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); + + /* 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_WRONG_OBJECT_TYPE), + errmsg("cannot inherit from temporary relation \"%s\"", + RelationGetRelationName(parent_rel)))); + + /* 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"))); + + /* 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"))); + + /* 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))); + + /* Likewise for partitions */ + if (parent_rel->rd_rel->relispartition) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot inherit from a partition"))); + + /* + * 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); + + 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 + * 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."))); + + /* OK to create inheritance */ + CreateInheritance(child_rel, parent_rel, false); + + ObjectAddressSet(address, RelationRelationId, + RelationGetRelid(parent_rel)); + + /* keep our lock on the parent relation until commit */ + table_close(parent_rel, NoLock); + + 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); + + /* + * 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. + * + * Note: we do not reject the case where the child already inherits from + * the parent indirectly; CREATE TABLE doesn't reject comparable cases. + */ + ScanKeyInit(&key, + Anum_pg_inherits_inhrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(child_rel))); + scan = systable_beginscan(catalogRelation, InheritsRelidSeqnoIndexId, + true, NULL, 1, &key); + + /* inhseqno sequences start at 1 */ + inhseqno = 0; + while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan))) + { + Form_pg_inherits inh = (Form_pg_inherits) GETSTRUCT(inheritsTuple); + + 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)))); + + if (inh->inhseqno > inhseqno) + inhseqno = inh->inhseqno; + } + systable_endscan(scan); + + /* Match up the columns and bump attinhcount as needed */ + MergeAttributesIntoExisting(child_rel, parent_rel, ispartition); + + /* Match up the constraints and bump coninhcount as needed */ + MergeConstraintsIntoExisting(child_rel, parent_rel); + + /* + * 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); + + /* Now we're done with pg_inherits */ + table_close(catalogRelation, RowExclusiveLock); +} + +/* + * 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; + + 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); + + expr = DirectFunctionCall2(pg_get_expr, attr, + ObjectIdGetDatum(con->conrelid)); + return TextDatumGetCString(expr); +} + +/* + * Determine whether two check constraints are functionally equivalent + * + * The test we apply is to see whether they reverse-compile to the same + * source string. This insulates us from issues like whether attributes + * have the same physical column numbers in parent and child relations. + * + * Note that we ignore enforceability as there are cases where constraints + * with differing enforceability are allowed. + */ +static bool +constraints_equivalent(HeapTuple a, HeapTuple b, TupleDesc tupleDesc) +{ + Form_pg_constraint acon = (Form_pg_constraint) GETSTRUCT(a); + Form_pg_constraint bcon = (Form_pg_constraint) GETSTRUCT(b); + + if (acon->condeferrable != bcon->condeferrable || + acon->condeferred != bcon->condeferred || + strcmp(decompile_conbin(a, tupleDesc), + decompile_conbin(b, tupleDesc)) != 0) + return false; + else + return true; +} + +/* + * Check columns in child table match up with columns in parent, and increment + * their attinhcount. + * + * Called by CreateInheritance + * + * Currently all parent columns must be found in child. Missing columns are an + * error. One day we might consider creating new columns like CREATE TABLE + * does. However, that is widely unpopular --- in the common use case of + * partitioned tables it's a foot-gun. + * + * The data type must match exactly. If the parent column is NOT NULL then + * the child must be as well. Defaults are not compared, however. + */ +static void +MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispartition) +{ + Relation attrrel; + TupleDesc parent_desc; + + attrrel = table_open(AttributeRelationId, RowExclusiveLock); + parent_desc = RelationGetDescr(parent_rel); + + for (AttrNumber parent_attno = 1; parent_attno <= parent_desc->natts; parent_attno++) + { + Form_pg_attribute parent_att = TupleDescAttr(parent_desc, parent_attno - 1); + char *parent_attname = NameStr(parent_att->attname); + HeapTuple tuple; + + /* Ignore dropped columns in the parent. */ + if (parent_att->attisdropped) + continue; + + /* Find same column in child (matching on column name). */ + tuple = SearchSysCacheCopyAttName(RelationGetRelid(child_rel), parent_attname); + if (HeapTupleIsValid(tuple)) + { + Form_pg_attribute child_att = (Form_pg_attribute) GETSTRUCT(tuple); + + if (parent_att->atttypid != child_att->atttypid || + parent_att->atttypmod != child_att->atttypmod) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("child table \"%s\" has different type for column \"%s\"", + RelationGetRelationName(child_rel), parent_attname))); + + if (parent_att->attcollation != child_att->attcollation) + ereport(ERROR, + (errcode(ERRCODE_COLLATION_MISMATCH), + errmsg("child table \"%s\" has different collation for column \"%s\"", + RelationGetRelationName(child_rel), parent_attname))); + + /* + * If the parent has a not-null constraint that's not NO INHERIT, + * make sure the child has one too. + * + * Other constraints are checked elsewhere. + */ + if (parent_att->attnotnull && !child_att->attnotnull) + { + HeapTuple contup; + + contup = findNotNullConstraintAttnum(RelationGetRelid(parent_rel), + parent_att->attnum); + if (HeapTupleIsValid(contup) && + !((Form_pg_constraint) GETSTRUCT(contup))->connoinherit) + ereport(ERROR, + errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("column \"%s\" in child table \"%s\" must be marked NOT NULL", + parent_attname, RelationGetRelationName(child_rel))); + } + + /* + * Child column must be generated if and only if parent column is. + */ + if (parent_att->attgenerated && !child_att->attgenerated) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("column \"%s\" in child table must be a generated column", parent_attname))); + if (child_att->attgenerated && !parent_att->attgenerated) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("column \"%s\" in child table must not be a generated column", parent_attname))); + + if (parent_att->attgenerated && child_att->attgenerated && child_att->attgenerated != parent_att->attgenerated) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("column \"%s\" inherits from generated column of different kind", parent_attname), + errdetail("Parent column is %s, child column is %s.", + parent_att->attgenerated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL", + child_att->attgenerated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL"))); + + /* + * Regular inheritance children are independent enough not to + * inherit identity columns. But partitions are integral part of + * a partitioned table and inherit identity column. + */ + if (ispartition) + child_att->attidentity = parent_att->attidentity; + + /* + * OK, bump the child column's inheritance count. (If we fail + * later on, this change will just roll back.) + */ + if (pg_add_s16_overflow(child_att->attinhcount, 1, + &child_att->attinhcount)) + ereport(ERROR, + errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("too many inheritance parents")); + + /* + * In case of partitions, we must enforce that value of attislocal + * is same in all partitions. (Note: there are only inherited + * attributes in partitions) + */ + if (parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { + Assert(child_att->attinhcount == 1); + child_att->attislocal = false; + } + + CatalogTupleUpdate(attrrel, &tuple->t_self, tuple); + heap_freetuple(tuple); + } + else + { + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("child table is missing column \"%s\"", parent_attname))); + } + } + + table_close(attrrel, RowExclusiveLock); +} + +/* + * Check constraints in child table match up with constraints in parent, + * and increment their coninhcount. + * + * Constraints that are marked ONLY in the parent are ignored. + * + * Called by CreateInheritance + * + * Currently all constraints in parent must be present in the child. One day we + * may consider adding new constraints like CREATE TABLE does. + * + * XXX This is O(N^2) which may be an issue with tables with hundreds of + * constraints. As long as tables have more like 10 constraints it shouldn't be + * a problem though. Even 100 constraints ought not be the end of the world. + * + * XXX See MergeWithExistingConstraint too if you change this code. + */ +static void +MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel) +{ + Relation constraintrel; + SysScanDesc parent_scan; + ScanKeyData parent_key; + HeapTuple parent_tuple; + Oid parent_relid = RelationGetRelid(parent_rel); + AttrMap *attmap; + + constraintrel = table_open(ConstraintRelationId, RowExclusiveLock); + + /* Outer loop scans through the parent's constraint definitions */ + ScanKeyInit(&parent_key, + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(parent_relid)); + parent_scan = systable_beginscan(constraintrel, ConstraintRelidTypidNameIndexId, + true, NULL, 1, &parent_key); + + attmap = build_attrmap_by_name(RelationGetDescr(parent_rel), + RelationGetDescr(child_rel), + true); + + while (HeapTupleIsValid(parent_tuple = systable_getnext(parent_scan))) + { + Form_pg_constraint parent_con = (Form_pg_constraint) GETSTRUCT(parent_tuple); + SysScanDesc child_scan; + ScanKeyData child_key; + HeapTuple child_tuple; + AttrNumber parent_attno; + bool found = false; + + if (parent_con->contype != CONSTRAINT_CHECK && + parent_con->contype != CONSTRAINT_NOTNULL) + continue; + + /* if the parent's constraint is marked NO INHERIT, it's not inherited */ + if (parent_con->connoinherit) + continue; + + if (parent_con->contype == CONSTRAINT_NOTNULL) + parent_attno = extractNotNullColumn(parent_tuple); + else + parent_attno = InvalidAttrNumber; + + /* Search for a child constraint matching this one */ + ScanKeyInit(&child_key, + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(child_rel))); + child_scan = systable_beginscan(constraintrel, ConstraintRelidTypidNameIndexId, + true, NULL, 1, &child_key); + + while (HeapTupleIsValid(child_tuple = systable_getnext(child_scan))) + { + Form_pg_constraint child_con = (Form_pg_constraint) GETSTRUCT(child_tuple); + HeapTuple child_copy; + + if (child_con->contype != parent_con->contype) + continue; + + /* + * CHECK constraint are matched by constraint name, NOT NULL ones + * by attribute number. + */ + if (child_con->contype == CONSTRAINT_CHECK) + { + if (strcmp(NameStr(parent_con->conname), + NameStr(child_con->conname)) != 0) + continue; + } + else if (child_con->contype == CONSTRAINT_NOTNULL) + { + Form_pg_attribute parent_attr; + Form_pg_attribute child_attr; + AttrNumber child_attno; + + parent_attr = TupleDescAttr(parent_rel->rd_att, parent_attno - 1); + child_attno = extractNotNullColumn(child_tuple); + if (parent_attno != attmap->attnums[child_attno - 1]) + continue; + + child_attr = TupleDescAttr(child_rel->rd_att, child_attno - 1); + /* there shouldn't be constraints on dropped columns */ + if (parent_attr->attisdropped || child_attr->attisdropped) + elog(ERROR, "found not-null constraint on dropped columns"); + } + + if (child_con->contype == CONSTRAINT_CHECK && + !constraints_equivalent(parent_tuple, child_tuple, RelationGetDescr(constraintrel))) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("child table \"%s\" has different definition for check constraint \"%s\"", + RelationGetRelationName(child_rel), NameStr(parent_con->conname)))); + + /* + * If the child constraint is "no inherit" then cannot merge + */ + if (child_con->connoinherit) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("constraint \"%s\" conflicts with non-inherited constraint on child table \"%s\"", + NameStr(child_con->conname), RelationGetRelationName(child_rel)))); + + /* + * If the child constraint is "not valid" then cannot merge with a + * valid parent constraint + */ + if (parent_con->convalidated && child_con->conenforced && + !child_con->convalidated) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("constraint \"%s\" conflicts with NOT VALID constraint on child table \"%s\"", + NameStr(child_con->conname), RelationGetRelationName(child_rel)))); + + /* + * A NOT ENFORCED child constraint cannot be merged with an + * ENFORCED parent constraint. However, the reverse is allowed, + * where the child constraint is ENFORCED. + */ + if (parent_con->conenforced && !child_con->conenforced) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("constraint \"%s\" conflicts with NOT ENFORCED constraint on child table \"%s\"", + NameStr(child_con->conname), RelationGetRelationName(child_rel)))); + + /* + * OK, bump the child constraint's inheritance count. (If we fail + * later on, this change will just roll back.) + */ + child_copy = heap_copytuple(child_tuple); + child_con = (Form_pg_constraint) GETSTRUCT(child_copy); + + if (pg_add_s16_overflow(child_con->coninhcount, 1, + &child_con->coninhcount)) + ereport(ERROR, + errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("too many inheritance parents")); + + /* + * In case of partitions, an inherited constraint must be + * inherited only once since it cannot have multiple parents and + * it is never considered local. + */ + if (parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { + Assert(child_con->coninhcount == 1); + child_con->conislocal = false; + } + + CatalogTupleUpdate(constraintrel, &child_copy->t_self, child_copy); + heap_freetuple(child_copy); + + found = true; + break; + } + + systable_endscan(child_scan); + + if (!found) + { + if (parent_con->contype == CONSTRAINT_NOTNULL) + ereport(ERROR, + errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("column \"%s\" in child table \"%s\" must be marked NOT NULL", + get_attname(parent_relid, + extractNotNullColumn(parent_tuple), + false), + RelationGetRelationName(child_rel))); + + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("child table is missing constraint \"%s\"", + NameStr(parent_con->conname)))); + } + } + + systable_endscan(parent_scan); + table_close(constraintrel, RowExclusiveLock); +} + +/* + * ALTER TABLE NO INHERIT + * + * Return value is the address of the relation that is no longer parent. + */ +static ObjectAddress +ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode) +{ + ObjectAddress address; + Relation parent_rel; + + /* + * AccessShareLock on the parent is probably enough, seeing that DROP + * TABLE doesn't lock parent tables at all. We need some lock since we'll + * be inspecting the parent's schema. + */ + parent_rel = table_openrv(parent, AccessShareLock); + + /* + * We don't bother to check ownership of the parent table --- ownership of + * the child is presumed enough rights. + */ + + /* Off to RemoveInheritance() where most of the work happens */ + RemoveInheritance(rel, parent_rel, false); + + ObjectAddressSet(address, RelationRelationId, + RelationGetRelid(parent_rel)); + + /* keep our lock on the parent relation until commit */ + table_close(parent_rel, NoLock); + + return address; +} + +/* + * MarkInheritDetached + * + * Set inhdetachpending for a partition, for ATExecDetachPartition + * in concurrent mode. While at it, verify that no other partition is + * already pending detach. + */ +static void +MarkInheritDetached(Relation child_rel, Relation parent_rel) +{ + Relation catalogRelation; + SysScanDesc scan; + ScanKeyData key; + HeapTuple inheritsTuple; + bool found = false; + + Assert(parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); + + /* + * Find pg_inherits entries by inhparent. (We need to scan them all in + * order to verify that no other partition is pending detach.) + */ + catalogRelation = table_open(InheritsRelationId, RowExclusiveLock); + ScanKeyInit(&key, + Anum_pg_inherits_inhparent, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(parent_rel))); + scan = systable_beginscan(catalogRelation, InheritsParentIndexId, + true, NULL, 1, &key); + + while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan))) + { + Form_pg_inherits inhForm; + + inhForm = (Form_pg_inherits) GETSTRUCT(inheritsTuple); + if (inhForm->inhdetachpending) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("partition \"%s\" already pending detach in partitioned table \"%s.%s\"", + get_rel_name(inhForm->inhrelid), + get_namespace_name(parent_rel->rd_rel->relnamespace), + RelationGetRelationName(parent_rel)), + errhint("Use ALTER TABLE ... DETACH PARTITION ... FINALIZE to complete the pending detach operation.")); + + if (inhForm->inhrelid == RelationGetRelid(child_rel)) + { + HeapTuple newtup; + + newtup = heap_copytuple(inheritsTuple); + ((Form_pg_inherits) GETSTRUCT(newtup))->inhdetachpending = true; + + CatalogTupleUpdate(catalogRelation, + &inheritsTuple->t_self, + newtup); + found = true; + heap_freetuple(newtup); + /* keep looking, to ensure we catch others pending detach */ + } + } + + /* Done */ + systable_endscan(scan); + table_close(catalogRelation, RowExclusiveLock); + + if (!found) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_TABLE), + errmsg("relation \"%s\" is not a partition of relation \"%s\"", + RelationGetRelationName(child_rel), + RelationGetRelationName(parent_rel)))); +} + +/* + * RemoveInheritance + * + * Drop a parent from the child's parents. This just adjusts the attinhcount + * and attislocal of the columns and removes the pg_inherit and pg_depend + * entries. expect_detached is passed down to DeleteInheritsTuple, q.v.. + * + * If attinhcount goes to 0 then attislocal gets set to true. If it goes back + * up attislocal stays true, which means if a child is ever removed from a + * parent then its columns will never be automatically dropped which may + * surprise. But at least we'll never surprise by dropping columns someone + * isn't expecting to be dropped which would actually mean data loss. + * + * coninhcount and conislocal for inherited constraints are adjusted in + * exactly the same way. + * + * Common to ATExecDropInherit() and ATExecDetachPartition(). + */ +static void +RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached) +{ + Relation catalogRelation; + SysScanDesc scan; + ScanKeyData key[3]; + HeapTuple attributeTuple, + constraintTuple; + AttrMap *attmap; + List *connames; + List *nncolumns; + bool found; + bool is_partitioning; + + is_partitioning = (parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); + + found = DeleteInheritsTuple(RelationGetRelid(child_rel), + RelationGetRelid(parent_rel), + expect_detached, + RelationGetRelationName(child_rel)); + if (!found) + { + if (is_partitioning) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_TABLE), + errmsg("relation \"%s\" is not a partition of relation \"%s\"", + RelationGetRelationName(child_rel), + RelationGetRelationName(parent_rel)))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_TABLE), + errmsg("relation \"%s\" is not a parent of relation \"%s\"", + RelationGetRelationName(parent_rel), + RelationGetRelationName(child_rel)))); + } + + /* + * Search through child columns looking for ones matching parent rel + */ + catalogRelation = table_open(AttributeRelationId, RowExclusiveLock); + ScanKeyInit(&key[0], + Anum_pg_attribute_attrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(child_rel))); + scan = systable_beginscan(catalogRelation, AttributeRelidNumIndexId, + true, NULL, 1, key); + while (HeapTupleIsValid(attributeTuple = systable_getnext(scan))) + { + Form_pg_attribute att = (Form_pg_attribute) GETSTRUCT(attributeTuple); + + /* Ignore if dropped or not inherited */ + if (att->attisdropped) + continue; + if (att->attinhcount <= 0) + continue; + + if (SearchSysCacheExistsAttName(RelationGetRelid(parent_rel), + NameStr(att->attname))) + { + /* Decrement inhcount and possibly set islocal to true */ + HeapTuple copyTuple = heap_copytuple(attributeTuple); + Form_pg_attribute copy_att = (Form_pg_attribute) GETSTRUCT(copyTuple); + + copy_att->attinhcount--; + if (copy_att->attinhcount == 0) + copy_att->attislocal = true; + + CatalogTupleUpdate(catalogRelation, ©Tuple->t_self, copyTuple); + heap_freetuple(copyTuple); + } + } + systable_endscan(scan); + table_close(catalogRelation, RowExclusiveLock); + + /* + * Likewise, find inherited check and not-null constraints and disinherit + * them. To do this, we first need a list of the names of the parent's + * check constraints. (We cheat a bit by only checking for name matches, + * assuming that the expressions will match.) + * + * For NOT NULL columns, we store column numbers to match, mapping them in + * to the child rel's attribute numbers. + */ + attmap = build_attrmap_by_name(RelationGetDescr(child_rel), + RelationGetDescr(parent_rel), + false); + + catalogRelation = table_open(ConstraintRelationId, RowExclusiveLock); + ScanKeyInit(&key[0], + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(parent_rel))); + scan = systable_beginscan(catalogRelation, ConstraintRelidTypidNameIndexId, + true, NULL, 1, key); + + connames = NIL; + nncolumns = NIL; + + while (HeapTupleIsValid(constraintTuple = systable_getnext(scan))) + { + Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(constraintTuple); + + if (con->connoinherit) + continue; + + if (con->contype == CONSTRAINT_CHECK) + connames = lappend(connames, pstrdup(NameStr(con->conname))); + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(constraintTuple); + + nncolumns = lappend_int(nncolumns, attmap->attnums[parent_attno - 1]); + } + } + + systable_endscan(scan); + + /* Now scan the child's constraints to find matches */ + ScanKeyInit(&key[0], + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(child_rel))); + scan = systable_beginscan(catalogRelation, ConstraintRelidTypidNameIndexId, + true, NULL, 1, key); + + while (HeapTupleIsValid(constraintTuple = systable_getnext(scan))) + { + Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(constraintTuple); + bool match = false; + + /* + * Match CHECK constraints by name, not-null constraints by column + * number, and ignore all others. + */ + if (con->contype == CONSTRAINT_CHECK) + { + foreach_ptr(char, chkname, connames) + { + if (con->contype == CONSTRAINT_CHECK && + strcmp(NameStr(con->conname), chkname) == 0) + { + match = true; + connames = foreach_delete_current(connames, chkname); + break; + } + } + } + else if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber child_attno = extractNotNullColumn(constraintTuple); + + foreach_int(prevattno, nncolumns) + { + if (prevattno == child_attno) + { + match = true; + nncolumns = foreach_delete_current(nncolumns, prevattno); + break; + } + } + } + else + continue; + + if (match) + { + /* Decrement inhcount and possibly set islocal to true */ + HeapTuple copyTuple = heap_copytuple(constraintTuple); + Form_pg_constraint copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); + + if (copy_con->coninhcount <= 0) /* shouldn't happen */ + elog(ERROR, "relation %u has non-inherited constraint \"%s\"", + RelationGetRelid(child_rel), NameStr(copy_con->conname)); + + copy_con->coninhcount--; + if (copy_con->coninhcount == 0) + copy_con->conislocal = true; + + CatalogTupleUpdate(catalogRelation, ©Tuple->t_self, copyTuple); + heap_freetuple(copyTuple); + } + } + + /* We should have matched all constraints */ + if (connames != NIL || nncolumns != NIL) + elog(ERROR, "%d unmatched constraints while removing inheritance from \"%s\" to \"%s\"", + list_length(connames) + list_length(nncolumns), + RelationGetRelationName(child_rel), RelationGetRelationName(parent_rel)); + + systable_endscan(scan); + table_close(catalogRelation, RowExclusiveLock); + + drop_parent_dependency(RelationGetRelid(child_rel), + RelationRelationId, + RelationGetRelid(parent_rel), + child_dependency_type(is_partitioning)); + + /* + * Post alter hook of this inherits. Since object_access_hook doesn't take + * multiple object identifiers, we relay oid of parent relation using + * auxiliary_id argument. + */ + InvokeObjectPostAlterHookArg(InheritsRelationId, + RelationGetRelid(child_rel), 0, + RelationGetRelid(parent_rel), false); +} + +/* + * Drop the dependency created by StoreCatalogInheritance1 (CREATE TABLE + * INHERITS/ALTER TABLE INHERIT -- refclassid will be RelationRelationId) or + * heap_create_with_catalog (CREATE TABLE OF/ALTER TABLE OF -- refclassid will + * be TypeRelationId). There's no convenient way to do this, so go trawling + * through pg_depend. + */ +static void +drop_parent_dependency(Oid relid, Oid refclassid, Oid refobjid, + DependencyType deptype) +{ + Relation catalogRelation; + SysScanDesc scan; + ScanKeyData key[3]; + HeapTuple depTuple; + + catalogRelation = 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(relid)); + ScanKeyInit(&key[2], + Anum_pg_depend_objsubid, + BTEqualStrategyNumber, F_INT4EQ, + Int32GetDatum(0)); + + scan = systable_beginscan(catalogRelation, DependDependerIndexId, true, + NULL, 3, key); + + while (HeapTupleIsValid(depTuple = systable_getnext(scan))) + { + Form_pg_depend dep = (Form_pg_depend) GETSTRUCT(depTuple); + + if (dep->refclassid == refclassid && + dep->refobjid == refobjid && + dep->refobjsubid == 0 && + dep->deptype == deptype) + CatalogTupleDelete(catalogRelation, &depTuple->t_self); + } + + systable_endscan(scan); + table_close(catalogRelation, RowExclusiveLock); +} + +/* + * 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. + */ +static ObjectAddress +ATExecAddOf(Relation rel, const TypeName *ofTypename, LOCKMODE lockmode) +{ + 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; + + /* Validate the type. */ + typetuple = typenameType(NULL, ofTypename, NULL); + check_of_type(typetuple); + typeform = (Form_pg_type) GETSTRUCT(typetuple); + typeid = typeform->oid; + + /* 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_WRONG_OBJECT_TYPE), + errmsg("typed tables cannot inherit"))); + systable_endscan(scan); + table_close(inheritsRelation, AccessShareLock); + + /* + * Check the tuple descriptors for compatibility. Unlike inheritance, we + * require that the order also match. However, attnotnull need not match. + */ + typeTupleDesc = lookup_rowtype_tupdesc(typeid, -1); + tableTupleDesc = RelationGetDescr(rel); + table_attno = 1; + for (type_attno = 1; type_attno <= typeTupleDesc->natts; type_attno++) + { + Form_pg_attribute type_attr, + table_attr; + const char *type_attname, + *table_attname; + + /* 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); + + /* Get the next non-dropped table attribute. */ + do + { + 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_DATATYPE_MISMATCH), + errmsg("table has column \"%s\" where type requires \"%s\"", + table_attname, type_attname))); + + /* 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); + + /* 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 (!table_attr->attisdropped) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("table has extra column \"%s\"", + NameStr(table_attr->attname)))); + } + + /* 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); + + /* 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); + + /* 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); + + InvokeObjectPostAlterHook(RelationRelationId, relid, 0); + + heap_freetuple(classtuple); + table_close(relationRelation, RowExclusiveLock); + + ReleaseSysCache(typetuple); + + return typeobj; +} + +/* + * ALTER TABLE NOT OF + * + * Detach a typed table from its originating type. Just clear reloftype and + * remove the dependency. + */ +static void +ATExecDropOf(Relation rel, LOCKMODE lockmode) +{ + Oid relid = RelationGetRelid(rel); + Relation relationRelation; + HeapTuple tuple; + + if (!OidIsValid(rel->rd_rel->reloftype)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a typed table", + RelationGetRelationName(rel)))); + + /* + * We don't bother to check ownership of the type --- ownership of the + * table is presumed enough rights. No lock required on the type, either. + */ + + drop_parent_dependency(relid, TypeRelationId, rel->rd_rel->reloftype, + DEPENDENCY_NORMAL); + + /* Clear pg_class.reloftype */ + relationRelation = table_open(RelationRelationId, RowExclusiveLock); + tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for relation %u", relid); + ((Form_pg_class) GETSTRUCT(tuple))->reloftype = InvalidOid; + CatalogTupleUpdate(relationRelation, &tuple->t_self, tuple); + + InvokeObjectPostAlterHook(RelationRelationId, relid, 0); + + heap_freetuple(tuple); + table_close(relationRelation, RowExclusiveLock); +} + +/* + * relation_mark_replica_identity: Update a table's replica identity + * + * Iff ri_type = REPLICA_IDENTITY_INDEX, indexOid must be the Oid of a suitable + * index. Otherwise, it must be InvalidOid. + * + * Caller had better hold an exclusive lock on the relation, as the results + * of running two of these concurrently wouldn't be pretty. + */ +static void +relation_mark_replica_identity(Relation rel, char ri_type, Oid indexOid, + bool is_internal) +{ + Relation pg_index; + Relation pg_class; + HeapTuple pg_class_tuple; + HeapTuple pg_index_tuple; + Form_pg_class pg_class_form; + Form_pg_index pg_index_form; + ListCell *index; + + /* + * Check whether relreplident has changed, and update it if so. + */ + pg_class = table_open(RelationRelationId, RowExclusiveLock); + pg_class_tuple = SearchSysCacheCopy1(RELOID, + ObjectIdGetDatum(RelationGetRelid(rel))); + if (!HeapTupleIsValid(pg_class_tuple)) + elog(ERROR, "cache lookup failed for relation \"%s\"", + RelationGetRelationName(rel)); + pg_class_form = (Form_pg_class) GETSTRUCT(pg_class_tuple); + if (pg_class_form->relreplident != ri_type) + { + pg_class_form->relreplident = ri_type; + CatalogTupleUpdate(pg_class, &pg_class_tuple->t_self, pg_class_tuple); + } + table_close(pg_class, RowExclusiveLock); + heap_freetuple(pg_class_tuple); + + /* + * Update the per-index indisreplident flags correctly. + */ + pg_index = table_open(IndexRelationId, RowExclusiveLock); + foreach(index, RelationGetIndexList(rel)) + { + Oid thisIndexOid = lfirst_oid(index); + bool dirty = false; + + pg_index_tuple = SearchSysCacheCopy1(INDEXRELID, + ObjectIdGetDatum(thisIndexOid)); + if (!HeapTupleIsValid(pg_index_tuple)) + elog(ERROR, "cache lookup failed for index %u", thisIndexOid); + pg_index_form = (Form_pg_index) GETSTRUCT(pg_index_tuple); + + if (thisIndexOid == indexOid) + { + /* Set the bit if not already set. */ + if (!pg_index_form->indisreplident) + { + dirty = true; + pg_index_form->indisreplident = true; + } + } + else + { + /* Unset the bit if set. */ + if (pg_index_form->indisreplident) + { + dirty = true; + pg_index_form->indisreplident = false; + } + } + + if (dirty) + { + CatalogTupleUpdate(pg_index, &pg_index_tuple->t_self, pg_index_tuple); + InvokeObjectPostAlterHookArg(IndexRelationId, thisIndexOid, 0, + InvalidOid, is_internal); + + /* + * Invalidate the relcache for the table, so that after we commit + * all sessions will refresh the table's replica identity index + * before attempting any UPDATE or DELETE on the table. (If we + * changed the table's pg_class row above, then a relcache inval + * is already queued due to that; but we might not have.) + */ + CacheInvalidateRelcache(rel); + } + heap_freetuple(pg_index_tuple); + } + + table_close(pg_index, RowExclusiveLock); +} + +/* + * ALTER TABLE REPLICA IDENTITY ... + */ +static void +ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode) +{ + Oid indexOid; + Relation indexRel; + int key; + + if (stmt->identity_type == REPLICA_IDENTITY_DEFAULT) + { + relation_mark_replica_identity(rel, stmt->identity_type, InvalidOid, true); + return; + } + else if (stmt->identity_type == REPLICA_IDENTITY_FULL) + { + relation_mark_replica_identity(rel, stmt->identity_type, InvalidOid, true); + return; + } + else if (stmt->identity_type == REPLICA_IDENTITY_NOTHING) + { + relation_mark_replica_identity(rel, stmt->identity_type, InvalidOid, true); + return; + } + else if (stmt->identity_type == REPLICA_IDENTITY_INDEX) + { + /* fallthrough */ ; + } + else + elog(ERROR, "unexpected identity type %u", stmt->identity_type); + + /* Check that the index exists */ + indexOid = get_relname_relid(stmt->name, rel->rd_rel->relnamespace); + if (!OidIsValid(indexOid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("index \"%s\" for table \"%s\" does not exist", + stmt->name, RelationGetRelationName(rel)))); + + indexRel = index_open(indexOid, ShareLock); + + /* Check that the index is on the relation we're altering. */ + if (indexRel->rd_index == NULL || + indexRel->rd_index->indrelid != RelationGetRelid(rel)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not an index for table \"%s\"", + RelationGetRelationName(indexRel), + RelationGetRelationName(rel)))); + + /* + * The AM must support uniqueness, and the index must in fact be unique. + * If we have a WITHOUT OVERLAPS constraint (identified by uniqueness + + * exclusion), we can use that too. + */ + if ((!indexRel->rd_indam->amcanunique || + !indexRel->rd_index->indisunique) && + !(indexRel->rd_index->indisunique && indexRel->rd_index->indisexclusion)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot use non-unique index \"%s\" as replica identity", + RelationGetRelationName(indexRel)))); + /* Deferred indexes are not guaranteed to be always unique. */ + if (!indexRel->rd_index->indimmediate) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot use non-immediate index \"%s\" as replica identity", + RelationGetRelationName(indexRel)))); + /* Expression indexes aren't supported. */ + if (RelationGetIndexExpressions(indexRel) != NIL) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot use expression index \"%s\" as replica identity", + RelationGetRelationName(indexRel)))); + /* Predicate indexes aren't supported. */ + if (RelationGetIndexPredicate(indexRel) != NIL) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot use partial index \"%s\" as replica identity", + RelationGetRelationName(indexRel)))); + + /* Check index for nullable columns. */ + for (key = 0; key < IndexRelationGetNumberOfKeyAttributes(indexRel); key++) + { + int16 attno = indexRel->rd_index->indkey.values[key]; + Form_pg_attribute attr; + + /* + * Reject any other system columns. (Going forward, we'll disallow + * indexes containing such columns in the first place, but they might + * exist in older branches.) + */ + if (attno <= 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), + errmsg("index \"%s\" cannot be used as replica identity because column %d is a system column", + RelationGetRelationName(indexRel), attno))); + + attr = TupleDescAttr(rel->rd_att, attno - 1); + if (!attr->attnotnull) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("index \"%s\" cannot be used as replica identity because column \"%s\" is nullable", + RelationGetRelationName(indexRel), + NameStr(attr->attname)))); + } + + /* This index is suitable for use as a replica identity. Mark it. */ + relation_mark_replica_identity(rel, stmt->identity_type, indexOid, true); + + index_close(indexRel, NoLock); +} + +/* + * ALTER TABLE ENABLE/DISABLE ROW LEVEL SECURITY + */ +static void +ATExecSetRowSecurity(Relation rel, bool rls) +{ + Relation pg_class; + Oid relid; + HeapTuple tuple; + + relid = RelationGetRelid(rel); + + /* Pull the record for this relation and update it */ + pg_class = table_open(RelationRelationId, RowExclusiveLock); + + tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid)); + + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for relation %u", relid); + + ((Form_pg_class) GETSTRUCT(tuple))->relrowsecurity = rls; + CatalogTupleUpdate(pg_class, &tuple->t_self, tuple); + + InvokeObjectPostAlterHook(RelationRelationId, + RelationGetRelid(rel), 0); + + table_close(pg_class, RowExclusiveLock); + heap_freetuple(tuple); +} + +/* + * ALTER TABLE FORCE/NO FORCE ROW LEVEL SECURITY + */ +static void +ATExecForceNoForceRowSecurity(Relation rel, bool force_rls) +{ + Relation pg_class; + Oid relid; + HeapTuple tuple; + + relid = RelationGetRelid(rel); + + pg_class = table_open(RelationRelationId, RowExclusiveLock); + + tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid)); + + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for relation %u", relid); + + ((Form_pg_class) GETSTRUCT(tuple))->relforcerowsecurity = force_rls; + CatalogTupleUpdate(pg_class, &tuple->t_self, tuple); + + InvokeObjectPostAlterHook(RelationRelationId, + RelationGetRelid(rel), 0); + + table_close(pg_class, RowExclusiveLock); + heap_freetuple(tuple); +} + +/* + * ALTER FOREIGN TABLE OPTIONS (...) + */ +static void +ATExecGenericOptions(Relation rel, List *options) +{ + 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 (options == NIL) + return; + + ftrel = table_open(ForeignTableRelationId, RowExclusiveLock); + + 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); + + 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(FOREIGNTABLEREL, + tuple, + Anum_pg_foreign_table_ftoptions, + &isnull); + if (isnull) + datum = PointerGetDatum(NULL); + + /* Transform the options */ + datum = transformGenericOptions(ForeignTableRelationId, + datum, + options, + fdw->fdwvalidator); + + if (DatumGetPointer(datum) != NULL) + repl_val[Anum_pg_foreign_table_ftoptions - 1] = datum; + else + repl_null[Anum_pg_foreign_table_ftoptions - 1] = true; + + repl_repl[Anum_pg_foreign_table_ftoptions - 1] = true; + + /* Everything looks good - update the tuple */ + + tuple = heap_modify_tuple(tuple, RelationGetDescr(ftrel), + repl_val, repl_null, repl_repl); + + CatalogTupleUpdate(ftrel, &tuple->t_self, tuple); + + /* + * Invalidate relcache so that all sessions will refresh any cached plans + * that might depend on the old options. + */ + CacheInvalidateRelcache(rel); + + InvokeObjectPostAlterHook(ForeignTableRelationId, + RelationGetRelid(rel), 0); + + table_close(ftrel, RowExclusiveLock); + + heap_freetuple(tuple); +} + +/* + * ALTER TABLE ALTER COLUMN SET COMPRESSION + * + * Return value is the address of the modified column + */ +static ObjectAddress +ATExecSetCompression(Relation rel, + const char *column, + Node *newValue, + LOCKMODE lockmode) +{ + Relation attrel; + HeapTuple tuple; + Form_pg_attribute atttableform; + AttrNumber attnum; + char *compression; + char cmethod; + ObjectAddress address; + + compression = strVal(newValue); + + attrel = table_open(AttributeRelationId, RowExclusiveLock); + + /* 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)))); + + /* 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\"", column))); + + /* + * Check that column type is compressible, then get the attribute + * compression method code + */ + cmethod = GetAttributeCompression(atttableform->atttypid, compression); + + /* update pg_attribute entry */ + atttableform->attcompression = cmethod; + CatalogTupleUpdate(attrel, &tuple->t_self, tuple); + + InvokeObjectPostAlterHook(RelationRelationId, + RelationGetRelid(rel), + attnum); + + /* + * Apply the change to indexes as well (only for simple index columns, + * matching behavior of index.c ConstructTupleDescriptor()). + */ + SetIndexStorageProperties(rel, attrel, attnum, + false, 0, + true, cmethod, + lockmode); + + heap_freetuple(tuple); + + table_close(attrel, RowExclusiveLock); + + /* make changes visible */ + CommandCounterIncrement(); + + ObjectAddressSubSet(address, RelationRelationId, + RelationGetRelid(rel), attnum); + return address; +} + + +/* + * 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 +ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged) +{ + Relation pg_constraint; + HeapTuple tuple; + SysScanDesc scan; + ScanKeyData skey[1]; + + /* + * 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) + { + 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; + } + + /* + * Check that the table is not part of any publication when changing to + * UNLOGGED, as UNLOGGED tables can't be published. + */ + if (!toLogged && + GetRelationIncludedPublications(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."))); + + /* + * 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); + + /* + * 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); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(tuple); + + if (con->contype == CONSTRAINT_FOREIGN) + { + Oid foreignrelid; + Relation foreignrel; + + /* the opposite end of what we used as scankey */ + foreignrelid = toLogged ? con->confrelid : con->conrelid; + + /* ignore if self-referencing */ + if (RelationGetRelid(rel) == foreignrelid) + continue; + + foreignrel = relation_open(foreignrelid, AccessShareLock); + + if (toLogged) + { + 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 + { + if (RelationIsPermanent(foreignrel)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("could not change table \"%s\" to unlogged because it references logged table \"%s\"", + RelationGetRelationName(rel), + RelationGetRelationName(foreignrel)), + errtableconstraint(rel, NameStr(con->conname)))); + } + + relation_close(foreignrel, AccessShareLock); + } + } + + systable_endscan(scan); + + table_close(pg_constraint, AccessShareLock); + + /* force rewrite if necessary; see comment in ATRewriteTables */ + tab->rewrite |= AT_REWRITE_ALTER_PERSISTENCE; + if (toLogged) + tab->newrelpersistence = RELPERSISTENCE_PERMANENT; + else + tab->newrelpersistence = RELPERSISTENCE_UNLOGGED; + tab->chgPersistence = true; +} + +/* + * Execute ALTER TABLE SET SCHEMA + */ +ObjectAddress +AlterTableNamespace(AlterObjectSchemaStmt *stmt, Oid *oldschema) +{ + Relation rel; + Oid relid; + Oid oldNspOid; + Oid nspOid; + RangeVar *newrv; + ObjectAddresses *objsMoved; + ObjectAddress myself; + + relid = RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock, + stmt->missing_ok ? RVR_MISSING_OK : 0, + RangeVarCallbackForAlterRelation, + stmt); + + if (!OidIsValid(relid)) + { + ereport(NOTICE, + (errmsg("relation \"%s\" does not exist, skipping", + stmt->relation->relname))); + return InvalidObjectAddress; + } + + rel = relation_open(relid, NoLock); + + oldNspOid = RelationGetNamespace(rel); + + /* If it's an owned sequence, disallow moving it by itself. */ + if (rel->rd_rel->relkind == RELKIND_SEQUENCE) + { + Oid tableId; + int32 colId; + + if (sequenceIsOwned(relid, DEPENDENCY_AUTO, &tableId, &colId) || + sequenceIsOwned(relid, DEPENDENCY_INTERNAL, &tableId, &colId)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot move an owned sequence into another schema"), + errdetail("Sequence \"%s\" is linked to table \"%s\".", + RelationGetRelationName(rel), + get_rel_name(tableId)))); + } + + /* Get and lock schema OID and check its permissions. */ + newrv = makeRangeVar(stmt->newschema, RelationGetRelationName(rel), -1); + nspOid = RangeVarGetAndCheckCreationNamespace(newrv, NoLock, NULL); + + /* common checks on switching namespaces */ + CheckSetNamespace(oldNspOid, nspOid); + + objsMoved = new_object_addresses(); + AlterTableNamespaceInternal(rel, oldNspOid, nspOid, objsMoved); + free_object_addresses(objsMoved); + + ObjectAddressSet(myself, RelationRelationId, relid); + + if (oldschema) + *oldschema = oldNspOid; + + /* close rel, but keep lock until commit */ + relation_close(rel, NoLock); + + return myself; +} + +/* + * The guts of relocating a table or materialized view to another namespace: + * besides moving the relation itself, its dependent objects are relocated to + * the new schema. + */ +void +AlterTableNamespaceInternal(Relation rel, Oid oldNspOid, Oid nspOid, + ObjectAddresses *objsMoved) +{ + Relation classRel; + + Assert(objsMoved != NULL); + + /* OK, modify the pg_class row and pg_depend entry */ + classRel = table_open(RelationRelationId, RowExclusiveLock); + + AlterRelationNamespaceInternal(classRel, RelationGetRelid(rel), oldNspOid, + nspOid, true, objsMoved); + + /* Fix the table's row type too, if it has one */ + if (OidIsValid(rel->rd_rel->reltype)) + AlterTypeNamespaceInternal(rel->rd_rel->reltype, nspOid, + false, /* isImplicitArray */ + false, /* ignoreDependent */ + false, /* errorOnTableType */ + objsMoved); + + /* Fix other dependent stuff */ + AlterIndexNamespaces(classRel, rel, oldNspOid, nspOid, objsMoved); + AlterSeqNamespaces(classRel, rel, oldNspOid, nspOid, + objsMoved, AccessExclusiveLock); + AlterConstraintNamespaces(RelationGetRelid(rel), oldNspOid, nspOid, + false, objsMoved); + + table_close(classRel, RowExclusiveLock); +} + +/* + * 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. + */ +void +AlterRelationNamespaceInternal(Relation classRel, Oid relOid, + Oid oldNspOid, Oid newNspOid, + bool hasDependEntry, + ObjectAddresses *objsMoved) +{ + HeapTuple classTup; + Form_pg_class classForm; + ObjectAddress thisobj; + bool already_done = false; + + /* 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); + + Assert(classForm->relnamespace == oldNspOid); + + thisobj.classId = RelationRelationId; + thisobj.objectId = relOid; + thisobj.objectSubId = 0; + + /* + * 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. + */ + already_done = object_address_present(&thisobj, objsMoved); + if (!already_done && oldNspOid != newNspOid) + { + ItemPointerData otid = classTup->t_self; + + /* check for duplicate name (more friendly than unique-index failure) */ + if (get_relname_relid(NameStr(classForm->relname), + newNspOid) != InvalidOid) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("relation \"%s\" already exists in schema \"%s\"", + NameStr(classForm->relname), + get_namespace_name(newNspOid)))); + + /* classTup is a copy, so OK to scribble on */ + classForm->relnamespace = newNspOid; + + CatalogTupleUpdate(classRel, &otid, classTup); + UnlockTuple(classRel, &otid, InplaceUpdateTupleLock); + + + /* 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) + { + add_exact_object_address(&thisobj, objsMoved); + + InvokeObjectPostAlterHook(RelationRelationId, relOid, 0); + } + + heap_freetuple(classTup); +} + +/* + * 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; + + indexList = RelationGetIndexList(rel); + + foreach(l, indexList) + { + Oid indexOid = lfirst_oid(l); + ObjectAddress thisobj; + + thisobj.classId = RelationRelationId; + thisobj.objectId = indexOid; + thisobj.objectSubId = 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); + } + } + + 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; + + /* + * 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(RelationGetRelid(rel))); + /* 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; + } + + /* 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); + + /* Now we can close it. Keep the lock till end of transaction. */ + relation_close(seqRel, NoLock); + } + + systable_endscan(scan); + + relation_close(depRel, AccessShareLock); +} + + +/* + * 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. + */ + +/* + * Register a newly-created relation's ON COMMIT action. + */ +void +register_on_commit_action(Oid relid, OnCommitAction action) +{ + OnCommitItem *oc; + MemoryContext oldcxt; + + /* + * We needn't bother registering the relation unless there is an ON COMMIT + * action we need to take. + */ + if (action == ONCOMMIT_NOOP || action == ONCOMMIT_PRESERVE_ROWS) + return; + + oldcxt = MemoryContextSwitchTo(CacheMemoryContext); + + 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); + + MemoryContextSwitchTo(oldcxt); +} + +/* + * Unregister any ON COMMIT action when a relation is deleted. + * + * Actually, we only mark the OnCommitItem entry as to be deleted after commit. + */ +void +remove_on_commit_action(Oid relid) +{ + ListCell *l; + + foreach(l, on_commits) + { + OnCommitItem *oc = (OnCommitItem *) lfirst(l); + + if (oc->relid == relid) + { + oc->deleting_subid = GetCurrentSubTransactionId(); + break; + } + } +} + +/* + * Perform ON COMMIT actions. + * + * This is invoked just before actually committing, since it's possible + * to encounter errors. + */ +void +PreCommit_on_commit_actions(void) +{ + ListCell *l; + List *oids_to_truncate = NIL; + List *oids_to_drop = NIL; + + foreach(l, on_commits) + { + OnCommitItem *oc = (OnCommitItem *) lfirst(l); + + /* Ignore entry if already dropped in this xact */ + if (oc->deleting_subid != InvalidSubTransactionId) + continue; + + switch (oc->oncommit) + { + case ONCOMMIT_NOOP: + case ONCOMMIT_PRESERVE_ROWS: + /* Do nothing (there shouldn't be such entries, actually) */ + break; + case ONCOMMIT_DELETE_ROWS: + + /* + * 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; + } + } + + /* + * 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); + + if (oids_to_drop != NIL) + { + ObjectAddresses *targetObjects = new_object_addresses(); + + foreach(l, oids_to_drop) + { + ObjectAddress object; + + object.classId = RelationRelationId; + object.objectId = lfirst_oid(l); + object.objectSubId = 0; + + Assert(!object_address_present(&object, targetObjects)); + + add_exact_object_address(&object, targetObjects); + } + + /* + * Object deletion might involve toast table access (to clean up + * toasted catalog entries), so ensure we have a valid snapshot. + */ + PushActiveSnapshot(GetTransactionSnapshot()); + + /* + * 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); + + PopActiveSnapshot(); + +#ifdef USE_ASSERT_CHECKING + + /* + * 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); + + if (oc->oncommit != ONCOMMIT_DROP) + continue; + + Assert(oc->deleting_subid != InvalidSubTransactionId); + } +#endif + } +} + +/* + * 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; + + foreach(cur_item, on_commits) + { + OnCommitItem *oc = (OnCommitItem *) lfirst(cur_item); + + if (isCommit ? oc->deleting_subid != InvalidSubTransactionId : + oc->creating_subid != InvalidSubTransactionId) + { + /* cur_item must be removed */ + on_commits = foreach_delete_current(on_commits, cur_item); + pfree(oc); + } + else + { + /* cur_item must be preserved */ + oc->creating_subid = InvalidSubTransactionId; + oc->deleting_subid = InvalidSubTransactionId; + } + } +} + +/* + * 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 (!isCommit && oc->creating_subid == mySubid) + { + /* cur_item must be removed */ + on_commits = foreach_delete_current(on_commits, cur_item); + pfree(oc); + } + else + { + /* 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); + - 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); + - 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) +{ - 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); - - /* 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; + + /* - * 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))); - - /* - * 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"))); - - /* - * 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)) - { - 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."))); - } ++ if (reltype == OBJECT_VIEW && relkind != RELKIND_VIEW) ++ ereport(ERROR, ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("\"%s\" is not a view", rv->relname))); + - ReleaseSysCache(tuple); - } ++ if (reltype == OBJECT_MATVIEW && relkind != RELKIND_MATVIEW) ++ ereport(ERROR, ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("\"%s\" is not a materialized view", rv->relname))); + - /* - * 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; ++ 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))); + - newspec = makeNode(PartitionSpec); ++ if (reltype == OBJECT_TYPE && relkind != RELKIND_COMPOSITE_TYPE) ++ ereport(ERROR, ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("\"%s\" is not a composite type", rv->relname))); + - newspec->strategy = partspec->strategy; - newspec->partParams = NIL; - newspec->location = partspec->location; ++ if (reltype == OBJECT_PROPGRAPH && relkind != RELKIND_PROPGRAPH) ++ ereport(ERROR, ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("\"%s\" is not a property graph", rv->relname))); + - /* Check valid number of columns for strategy */ - if (partspec->strategy == PARTITION_STRATEGY_LIST && - list_length(partspec->partParams) != 1) ++ if (reltype == OBJECT_INDEX && relkind != RELKIND_INDEX && ++ relkind != RELKIND_PARTITIONED_INDEX ++ && !IsA(stmt, RenameStmt)) + ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("cannot use \"list\" partition strategy with more than one column"))); ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("\"%s\" is not an index", rv->relname))); ++======= ++ newrelfilenumber = GetNewRelFileNumber(newTableSpace, NULL, ++ rel->rd_rel->relpersistence); ++>>>>>>> theirs ++ ++ /* 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); ++ } + + /* - * 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); +} + +/* - * Compute per-partition-column information from a list of PartitionElems. - * Expressions in the PartitionElems must be parse-analyzed already. ++ * 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 - ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNumber *partattrs, - List **partexprs, Oid *partopclass, Oid *partcollation, - PartitionStrategy strategy) ++ATExecSetTableSpaceNoStorage(Relation rel, Oid newTableSpace) +{ - int attn; - ListCell *lc; - Oid am_oid; ++ /* ++ * Shouldn't be called on relations having storage; these are processed in ++ * phase 3. ++ */ ++ Assert(!RELKIND_HAS_STORAGE(rel->rd_rel->relkind)); + - attn = 0; - foreach(lc, partParams) ++ /* check if relation can be moved to its new tablespace */ ++ if (!CheckRelationTableSpaceMove(rel, newTableSpace)) + { - PartitionElem *pelem = lfirst_node(PartitionElem, lc); - Oid atttype; - Oid attcollation; - - if (pelem->name != NULL) - { - /* Simple attribute reference */ - HeapTuple atttuple; - Form_pg_attribute attform; - - atttuple = SearchSysCacheAttName(RelationGetRelid(rel), - pelem->name); - if (!HeapTupleIsValid(atttuple)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("column \"%s\" named in partition key does not exist", - pelem->name), - parser_errposition(pstate, pelem->location))); - attform = (Form_pg_attribute) GETSTRUCT(atttuple); - - if (attform->attnum <= 0) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("cannot use system column \"%s\" in partition key", - pelem->name), - parser_errposition(pstate, pelem->location))); - - /* - * Stored generated columns cannot work: They are computed after - * BEFORE triggers, but partition routing is done before all - * triggers. Maybe virtual generated columns could be made to - * work, but then they would need to be handled as an expression - * below. - */ - if (attform->attgenerated) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("cannot use generated column in partition key"), - errdetail("Column \"%s\" is a generated column.", - pelem->name), - parser_errposition(pstate, pelem->location))); - - partattrs[attn] = attform->attnum; - atttype = attform->atttypid; - attcollation = attform->attcollation; - ReleaseSysCache(atttuple); - } - else - { - /* Expression */ - Node *expr = pelem->expr; - char partattname[16]; - Bitmapset *expr_attrs = NULL; - int i; ++ InvokeObjectPostAlterHook(RelationRelationId, ++ RelationGetRelid(rel), ++ 0); ++ return; ++ } + - Assert(expr != NULL); - atttype = exprType(expr); - attcollation = exprCollation(expr); ++ /* Update can be done, so change reltablespace */ ++ SetRelationTableSpace(rel, newTableSpace, InvalidOid); + - /* - * The expression must be of a storable type (e.g., not RECORD). - * The test is the same as for whether a table column is of a safe - * type (which is why we needn't check for the non-expression - * case). - */ - snprintf(partattname, sizeof(partattname), "%d", attn + 1); - CheckAttributeType(partattname, - atttype, attcollation, - NIL, CHKATYPE_IS_PARTKEY); ++ InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0); + - /* - * Strip any top-level COLLATE clause. This ensures that we treat - * "x COLLATE y" and "(x COLLATE y)" alike. - */ - while (IsA(expr, CollateExpr)) - expr = (Node *) ((CollateExpr *) expr)->arg; ++ /* Make sure the reltablespace change is visible */ ++ CommandCounterIncrement(); ++} + - /* - * Examine all the columns in the partition key expression. When - * the whole-row reference is present, examine all the columns of - * the partitioned table. - */ - pull_varattnos(expr, 1, &expr_attrs); - if (bms_is_member(0 - FirstLowInvalidHeapAttributeNumber, expr_attrs)) - { - expr_attrs = bms_add_range(expr_attrs, - 1 - FirstLowInvalidHeapAttributeNumber, - RelationGetNumberOfAttributes(rel) - FirstLowInvalidHeapAttributeNumber); - expr_attrs = bms_del_member(expr_attrs, 0 - FirstLowInvalidHeapAttributeNumber); - } ++/* ++ * 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); + - i = -1; - while ((i = bms_next_member(expr_attrs, i)) >= 0) - { - AttrNumber attno = i + FirstLowInvalidHeapAttributeNumber; ++ /* 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"))); + - Assert(attno != 0); ++ /* 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); + - /* - * Cannot allow system column references, since that would - * make partition routing impossible: their values won't be - * known yet when we need to do that. - */ - if (attno < 0) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("partition key expressions cannot contain system column references"))); ++ /* 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"))); + - /* - * Stored generated columns cannot work: They are computed - * after BEFORE triggers, but partition routing is done before - * all triggers. Virtual generated columns could probably - * work, but it would require more work elsewhere (for example - * SET EXPRESSION would need to check whether the column is - * used in partition keys). Seems safer to prohibit for now. - */ - if (TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("cannot use generated column in partition key"), - errdetail("Column \"%s\" is a generated column.", - get_attname(RelationGetRelid(rel), attno, false)), - parser_errposition(pstate, pelem->location))); - } ++ /* ++ * 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; + - if (IsA(expr, Var) && - ((Var *) expr)->varattno > 0) - { ++ aclresult = object_aclcheck(TableSpaceRelationId, new_tablespaceoid, GetUserId(), ++ ACL_CREATE); ++ if (aclresult != ACLCHECK_OK) ++ aclcheck_error(aclresult, OBJECT_TABLESPACE, ++ get_tablespace_name(new_tablespaceoid)); ++ } + - /* - * User wrote "(column)" or "(column COLLATE something)". - * Treat it like simple attribute anyway. - */ - partattrs[attn] = ((Var *) expr)->varattno; - } - else - { - partattrs[attn] = 0; /* marks the column as expression */ - *partexprs = lappend(*partexprs, expr); ++ /* ++ * 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; + - /* - * transformPartitionSpec() should have already rejected - * subqueries, aggregates, window functions, and SRFs, based - * on the EXPR_KIND_ for partition expressions. - */ ++ if (new_tablespaceoid == MyDatabaseTableSpace) ++ new_tablespaceoid = InvalidOid; + - /* - * Preprocess the expression before checking for mutability. - * This is essential for the reasons described in - * contain_mutable_functions_after_planning. However, we call - * expression_planner for ourselves rather than using that - * function, because if constant-folding reduces the - * expression to a constant, we'd like to know that so we can - * complain below. - * - * Like contain_mutable_functions_after_planning, assume that - * expression_planner won't scribble on its input, so this - * won't affect the partexprs entry we saved above. - */ - expr = (Node *) expression_planner((Expr *) expr); ++ /* no-op */ ++ if (orig_tablespaceoid == new_tablespaceoid) ++ return new_tablespaceoid; + - /* - * Partition expressions cannot contain mutable functions, - * because a given row must always map to the same partition - * as long as there is no change in the partition boundary - * structure. - */ - if (contain_mutable_functions(expr)) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("functions in partition key expression must be marked IMMUTABLE"))); ++ /* ++ * 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)); + - /* - * While it is not exactly *wrong* for a partition expression - * to be a constant, it seems better to reject such keys. - */ - if (IsA(expr, Const)) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("cannot use constant expression as partition key"))); - } - } ++ 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; + + /* - * Apply collation override if any ++ * 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, or TOAST ++ * (TOAST will be moved with the main table). + */ - if (pelem->collation) - attcollation = get_collation_oid(pelem->collation, false); ++ if (IsCatalogNamespace(relForm->relnamespace) || ++ relForm->relisshared || ++ isAnyTempNamespace(relForm->relnamespace) || ++ IsToastNamespace(relForm->relnamespace)) ++ continue; + - /* - * Check we have a collation iff it's a collatable type. The only - * expected failures here are (1) COLLATE applied to a noncollatable - * type, or (2) partition expression had an unresolved collation. But - * we might as well code this to be a complete consistency check. - */ - if (type_is_collatable(atttype)) - { - if (!OidIsValid(attcollation)) - ereport(ERROR, - (errcode(ERRCODE_INDETERMINATE_COLLATION), - errmsg("could not determine which collation to use for partition expression"), - errhint("Use the COLLATE clause to set the collation explicitly."))); - } - else - { - if (OidIsValid(attcollation)) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("collations are not supported by type %s", - format_type_be(atttype)))); - } ++ /* 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; + - partcollation[attn] = attcollation; ++ /* Check if we are only moving objects owned by certain roles */ ++ if (role_oids != NIL && !list_member_oid(role_oids, relForm->relowner)) ++ continue; + + /* - * Identify the appropriate operator class. For list and range - * partitioning, we use a btree operator class; hash partitioning uses - * a hash operator class. ++ * 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 (strategy == PARTITION_STRATEGY_HASH) - am_oid = HASH_AM_OID; - else - am_oid = BTREE_AM_OID; - - if (!pelem->opclass) - { - partopclass[attn] = GetDefaultOpClass(atttype, am_oid); ++ if (!object_ownercheck(RelationRelationId, relOid, GetUserId())) ++ aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relOid)), ++ NameStr(relForm->relname)); + - if (!OidIsValid(partopclass[attn])) - { - if (strategy == PARTITION_STRATEGY_HASH) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("data type %s has no default operator class for access method \"%s\"", - format_type_be(atttype), "hash"), - errhint("You must specify a hash operator class or define a default hash operator class for the data type."))); - else - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("data type %s has no default operator class for access method \"%s\"", - format_type_be(atttype), "btree"), - errhint("You must specify a btree operator class or define a default btree operator class for the data type."))); - } - } ++ 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 - partopclass[attn] = ResolveOpClass(pelem->opclass, - atttype, - am_oid == HASH_AM_OID ? "hash" : "btree", - am_oid); - - attn++; - } - } - - /* - * PartConstraintImpliedByRelConstraint - * Do scanrel's existing constraints imply the partition constraint? - * - * "Existing constraints" include its check constraints and column-level - * not-null constraints. partConstraint describes the partition constraint, - * in implicit-AND form. - */ - bool - PartConstraintImpliedByRelConstraint(Relation scanrel, - List *partConstraint) - { - List *existConstraint = NIL; - TupleConstr *constr = RelationGetDescr(scanrel)->constr; - int i; - - if (constr && constr->has_not_null) - { - int natts = scanrel->rd_att->natts; - - for (i = 1; i <= natts; i++) - { - CompactAttribute *att = TupleDescCompactAttr(scanrel->rd_att, i - 1); - - /* invalid not-null constraint must be ignored here */ - if (att->attnullability == ATTNULLABLE_VALID && !att->attisdropped) - { - Form_pg_attribute wholeatt = TupleDescAttr(scanrel->rd_att, i - 1); - NullTest *ntest = makeNode(NullTest); - - ntest->arg = (Expr *) makeVar(1, - i, - wholeatt->atttypid, - wholeatt->atttypmod, - wholeatt->attcollation, - 0); - ntest->nulltesttype = IS_NOT_NULL; ++ LockRelationOid(relOid, AccessExclusiveLock); + - /* - * argisrow=false is correct even for a composite column, - * because attnotnull does not represent a SQL-spec IS NOT - * NULL test in such a case, just IS DISTINCT FROM NULL. - */ - ntest->argisrow = false; - ntest->location = -1; - existConstraint = lappend(existConstraint, ntest); - } - } ++ /* Add to our list of objects to move */ ++ relations = lappend_oid(relations, relOid); + } + - return ConstraintImpliedByRelConstraint(scanrel, partConstraint, existConstraint); - } ++ table_endscan(scan); ++ table_close(rel, AccessShareLock); + - /* - * ConstraintImpliedByRelConstraint - * Do scanrel's existing constraints imply the given constraint? - * - * testConstraint is the constraint to validate. provenConstraint is a - * caller-provided list of conditions which this function may assume - * to be true. Both provenConstraint and testConstraint must be in - * implicit-AND form, must only contain immutable clauses, and must - * contain only Vars with varno = 1. - */ - bool - ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint, List *provenConstraint) - { - List *existConstraint = list_copy(provenConstraint); - TupleConstr *constr = RelationGetDescr(scanrel)->constr; - int num_check, - i; ++ 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)))); + - num_check = (constr != NULL) ? constr->num_check : 0; - for (i = 0; i < num_check; i++) ++ /* Everything is locked, loop through and move all of the relations. */ ++ foreach(l, relations) + { - Node *cexpr; - - /* - * If this constraint hasn't been fully validated yet, we must ignore - * it here. - */ - if (!constr->check[i].ccvalid) - continue; - - /* - * NOT ENFORCED constraints are always marked as invalid, which should - * have been ignored. - */ - Assert(constr->check[i].ccenforced); ++ List *cmds = NIL; ++ AlterTableCmd *cmd = makeNode(AlterTableCmd); + - cexpr = stringToNode(constr->check[i].ccbin); ++ cmd->subtype = AT_SetTableSpace; ++ cmd->name = stmt->new_tablespacename; + - /* - * Run each expression through const-simplification and - * canonicalization. It is necessary, because we will be comparing it - * to similarly-processed partition constraint expressions, and may - * fail to detect valid matches without this. - */ - cexpr = eval_const_expressions(NULL, cexpr); - cexpr = (Node *) canonicalize_qual((Expr *) cexpr, true); ++ cmds = lappend(cmds, cmd); + - existConstraint = list_concat(existConstraint, - make_ands_implicit((Expr *) cexpr)); ++ EventTriggerAlterTableStart((Node *) stmt); ++ /* OID is set by AlterTableInternal */ ++ AlterTableInternal(lfirst_oid(l), cmds, false); ++ EventTriggerAlterTableEnd(); + } + - /* - * Try to make the proof. Since we are comparing CHECK constraints, we - * need to use weak implication, i.e., we assume existConstraint is - * not-false and try to prove the same for testConstraint. - * - * Note that predicate_implied_by assumes its first argument is known - * immutable. That should always be true for both NOT NULL and partition - * constraints, so we don't test it here. - */ - return predicate_implied_by(testConstraint, existConstraint, true); ++ return new_tablespaceoid; +} + - /* - * QueuePartitionConstraintValidation - * - * Add an entry to wqueue to have the given partition constraint validated by - * Phase 3, for the given relation, and all its children. - * - * We first verify whether the given constraint is implied by pre-existing - * relation constraints; if it is, there's no need to scan the table to - * validate, so don't queue in that case. - */ +static void - QueuePartitionConstraintValidation(List **wqueue, Relation scanrel, - List *partConstraint, - bool validate_default) ++index_copy_data(Relation rel, RelFileLocator newrlocator) +{ ++ SMgrRelation dstrel; ++ + /* - * Based on the table's existing constraints, determine whether or not we - * may skip scanning the table. ++ * 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. + */ - if (PartConstraintImpliedByRelConstraint(scanrel, partConstraint)) - { - if (!validate_default) - ereport(DEBUG1, - (errmsg_internal("partition constraint for table \"%s\" is implied by existing constraints", - RelationGetRelationName(scanrel)))); - else - ereport(DEBUG1, - (errmsg_internal("updated partition constraint for default partition \"%s\" is implied by existing constraints", - RelationGetRelationName(scanrel)))); - return; - } ++ FlushRelationBuffers(rel); + + /* - * Constraints proved insufficient. For plain relations, queue a - * validation item now; for partitioned tables, recurse to process each - * partition. ++ * 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(). + */ - if (scanrel->rd_rel->relkind == RELKIND_RELATION) - { - AlteredTableInfo *tab; ++ dstrel = RelationCreateStorage(newrlocator, rel->rd_rel->relpersistence, true); + - /* Grab a work queue entry. */ - tab = ATGetQueueEntry(wqueue, scanrel); - Assert(tab->partition_constraint == NULL); - tab->partition_constraint = (Expr *) linitial(partConstraint); - tab->validate_default = validate_default; - } - else if (scanrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) - { - PartitionDesc partdesc = RelationGetPartitionDesc(scanrel, true); - int i; ++ /* copy main fork */ ++ RelationCopyStorage(RelationGetSmgr(rel), dstrel, MAIN_FORKNUM, ++ rel->rd_rel->relpersistence); + - for (i = 0; i < partdesc->nparts; i++) ++ /* copy those extra forks that exist */ ++ for (ForkNumber forkNum = MAIN_FORKNUM + 1; ++ forkNum <= MAX_FORKNUM; forkNum++) ++ { ++ if (smgrexists(RelationGetSmgr(rel), forkNum)) + { - Relation part_rel; - List *thisPartConstraint; - - /* - * This is the minimum lock we need to prevent deadlocks. - */ - part_rel = table_open(partdesc->oids[i], AccessExclusiveLock); ++ smgrcreate(dstrel, forkNum, false); + + /* - * Adjust the constraint for scanrel so that it matches this - * partition's attribute numbers. ++ * WAL log creation if the relation is persistent, or this is the ++ * init fork of an unlogged relation. + */ - thisPartConstraint = - map_partition_varattnos(partConstraint, 1, - part_rel, scanrel); - - QueuePartitionConstraintValidation(wqueue, part_rel, - thisPartConstraint, - validate_default); - table_close(part_rel, NoLock); /* keep lock till commit */ ++ 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); + } + } ++ ++ /* drop old relation, and close new one */ ++ RelationDropStorage(rel); ++ smgrclose(dstrel); +} + +/* - * attachPartitionTable: attach a new partition to the partitioned table ++ * ALTER TABLE ENABLE/DISABLE TRIGGER + * - * wqueue: the ALTER TABLE work queue; can be NULL when not running as part - * of an ALTER TABLE sequence. - * rel: partitioned relation; - * attachrel: relation of attached partition; - * bound: bounds of attached relation. ++ * We just pass this off to trigger.c. + */ +static void - attachPartitionTable(List **wqueue, Relation rel, Relation attachrel, PartitionBoundSpec *bound) ++ATExecEnableDisableTrigger(Relation rel, const char *trigname, ++ char fires_when, bool skip_system, bool recurse, ++ LOCKMODE lockmode) +{ - /* - * Create an inheritance; the relevant checks are performed inside the - * function. - */ - CreateInheritance(attachrel, rel, true); - - /* Update the pg_class entry. */ - StorePartitionBound(attachrel, rel, bound); ++ EnableDisableTrigger(rel, trigname, InvalidOid, ++ fires_when, skip_system, recurse, ++ lockmode); + - /* Ensure there exists a correct set of indexes in the partition. */ - AttachPartitionEnsureIndexes(wqueue, rel, attachrel); ++ InvokeObjectPostAlterHook(RelationRelationId, ++ RelationGetRelid(rel), 0); ++} + - /* and triggers */ - CloneRowTriggersToPartition(rel, attachrel); ++/* ++ * 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); + - /* - * Clone foreign key constraints. Callee is responsible for setting up - * for phase 3 constraint verification. - */ - CloneForeignKeyConstraints(wqueue, rel, attachrel); ++ InvokeObjectPostAlterHook(RelationRelationId, ++ RelationGetRelid(rel), 0); +} + +/* - * 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) + { + /* + * translator: This is a separator in a list of publication + * names. + */ + appendStringInfoString(&pubnames, _(", ")); + } + + first = false; + + appendStringInfo(&pubnames, _("\"%s\""), pubname); + } + + 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. - */ - 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; - - 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."))); - } + * 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); + + 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 @@@ -21843,259 -16990,149 +20265,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; } /* @@@ -22539,409 -17444,259 +20789,341 @@@ 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); - /* Add a pre-cooked default expression. */ - StoreAttrDefault(newRel, num, def, true); + /* Transform the options */ + datum = transformGenericOptions(ForeignTableRelationId, + datum, + options, + fdw->fdwvalidator); - /* - * 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); - } - } - } + if (DatumGetPointer(datum) != NULL) + repl_val[Anum_pg_foreign_table_ftoptions - 1] = datum; + else + repl_null[Anum_pg_foreign_table_ftoptions - 1] = true; ++<<<<<<< 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, true, 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; + CacheInvalidateRelcache(rel); - /* - * The "include_noinh" argument is false because a partitioned table - * can't have NO INHERIT constraint. - */ - nnconstraints = RelationGetNotNullConstraints(RelationGetRelid(parent_rel), - false, false); + InvokeObjectPostAlterHook(ForeignTableRelationId, + RelationGetRelid(rel), 0); - Assert(list_length(nnconstraints) > 0); + table_close(ftrel, RowExclusiveLock); - /* - * We already set pg_attribute.attnotnull in createPartitionTable. No - * need call set_attnotnull again. - */ - AddRelationNewConstraints(newRel, NIL, nnconstraints, false, true, true, NULL); - } + heap_freetuple(tuple); } /* - * 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) { - Relation newRel; - Oid newRelId; - Oid existingRelid; - 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; - /* 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); - /* 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))); - - /* Create the relation. */ - newRelId = heap_create_with_catalog(newPartName->relname, - namespaceId, - parent_relform->reltablespace, - InvalidOid, - InvalidOid, - InvalidOid, - ownerId, - relamId, - descriptor, - NIL, - RELKIND_RELATION, - newPartName->relpersistence, - false, - false, - ONCOMMIT_NOOP, - (Datum) 0, - true, - allowSystemTableMods, - true, - InvalidOid, - NULL); + /* update pg_attribute entry */ + atttableform->attcompression = cmethod; + CatalogTupleUpdate(attrel, &tuple->t_self, tuple); - /* - * We must bump the command counter to make the newly-created relation - * tuple visible for opening. - */ - CommandCounterIncrement(); + InvokeObjectPostAlterHook(RelationRelationId, + RelationGetRelid(rel), + attnum); /* - * 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()). */ - 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 { @@@ -23038,637 -17833,430 +21260,759 @@@ 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; - performDeletionCheck(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); + /* + * 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); + } } - /* - * 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); + + oc = palloc_object(OnCommitItem); + oc->relid = relid; + oc->oncommit = action; + oc->creating_subid = GetCurrentSubTransactionId(); + oc->deleting_subid = InvalidSubTransactionId; - /* Create a destination tuple slot for the new partition. */ - pc->dstslot = table_slot_create(pc->partRel, NULL); + /* + * 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 { @@@ -23728,171 -18366,216 +22122,276 @@@ 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 - defaultPartOid = get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true)); + /* Must own relation. */ + if (!object_ownercheck(RelationRelationId, relid, GetUserId())) + aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relid)), rv->relname); + + /* 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. */ - object.objectId = splitRelOid; - object.classId = RelationRelationId; - object.objectSubId = 0; - performDeletionCheck(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); + 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"))); /* - * 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; }