=== Applying patches on top of PostgreSQL commit ID 6831cd9e3b082d7b830c3196742dd49e3540c49b === /etc/rc.d/jail: WARNING: Per-jail configuration via jail_* variables is obsolete. Please consider migrating to /etc/jail.conf. Fri Jan 16 19:19:20 UTC 2026 On branch cf/6269 nothing to commit, working tree clean === using 'git am' to apply patch ./v1-0001-move-partition-code-in-tablecmds.c-to-new-file.patch === Applying: move partition code in tablecmds.c to new file Using index info to reconstruct a base tree... 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 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 partition code in tablecmds.c to new file 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". 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/partcmds.c Removing src/include/commands/partcmds.h === using patch(1) to apply patch ./v1-0001-move-partition-code-in-tablecmds.c-to-new-file.patch === patching file src/backend/commands/Makefile patching file src/backend/commands/meson.build patching file src/backend/commands/partcmds.c patching file src/backend/commands/tablecmds.c Hunk #12 succeeded at 6395 (offset 33 lines). Hunk #13 succeeded at 6576 (offset 37 lines). Hunk #14 succeeded at 8325 (offset 37 lines). Hunk #15 succeeded at 10560 (offset 38 lines). Hunk #16 succeeded at 10738 (offset 38 lines). Hunk #17 succeeded at 10876 (offset 38 lines). Hunk #18 succeeded at 11042 (offset 38 lines). Hunk #19 succeeded at 11115 (offset 38 lines). Hunk #20 succeeded at 11925 (offset 38 lines). Hunk #21 succeeded at 16299 (offset 38 lines). Hunk #22 succeeded at 16785 (offset 38 lines). Hunk #23 succeeded at 16803 (offset 38 lines). Hunk #24 FAILED at 18537. 1 out of 24 hunks FAILED -- saving rejects to file src/backend/commands/tablecmds.c.rej patching file src/backend/partitioning/partbounds.c Hunk #1 succeeded at 21 (offset 1 line). patching file src/include/commands/partcmds.h patching file src/include/commands/tablecmds.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/partcmds.c Removing src/backend/commands/tablecmds.c.rej Removing src/include/commands/partcmds.h === using 'git apply' to apply patch ./v1-0001-move-partition-code-in-tablecmds.c-to-new-file.patch === Applied patch to 'src/backend/commands/Makefile' cleanly. Applied patch to 'src/backend/commands/meson.build' cleanly. Falling back to direct application... Applied patch to 'src/backend/commands/tablecmds.c' with conflicts. Applied patch to 'src/backend/partitioning/partbounds.c' cleanly. Falling back to direct application... Applied patch to 'src/include/commands/tablecmds.h' cleanly. U src/backend/commands/tablecmds.c diff --cc src/backend/commands/tablecmds.c index f976c0e5c7e,c93d022aa6d..00000000000 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@@ -12416,226 -11934,172 +11972,182 @@@ QueueFKConstraintValidation(List **wque } /* - * Do the catalog work for the inheritability change. + * If the table at either end of the constraint is partitioned, we need to + * recurse and handle every unvalidate constraint that is a child of this + * constraint. */ - if (cmdcon->alterInheritability && - ATExecAlterConstrInheritability(wqueue, cmdcon, conrel, rel, contuple, - lockmode)) - changed = true; + if (fkrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE || + get_rel_relkind(con->confrelid) == RELKIND_PARTITIONED_TABLE) + { + ScanKeyData pkey; + SysScanDesc pscan; + HeapTuple childtup; - return changed; - } + ScanKeyInit(&pkey, + Anum_pg_constraint_conparentid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(con->oid)); - /* - * Returns true if the constraint's enforceability is altered. - * - * Depending on whether the constraint is being set to ENFORCED or NOT - * ENFORCED, it creates or drops the trigger accordingly. - * - * 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. - */ - static bool - ATExecAlterConstrEnforceability(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; + pscan = systable_beginscan(conrel, ConstraintParentIndexId, + true, NULL, 1, &pkey); - /* Since this function recurses, it could be driven to stack overflow */ - check_stack_depth(); + while (HeapTupleIsValid(childtup = systable_getnext(pscan))) + { + Form_pg_constraint childcon; + Relation childrel; - Assert(cmdcon->alterEnforceability); + childcon = (Form_pg_constraint) GETSTRUCT(childtup); - 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; - /* Should be foreign key constraint */ - Assert(currcon->contype == CONSTRAINT_FOREIGN); + childrel = table_open(childcon->conrelid, lockmode); - rel = table_open(currcon->conrelid, lockmode); + /* + * NB: Note that pkrelid should be passed as-is during recursion, + * as it is required to identify the root referenced table. + */ + QueueFKConstraintValidation(wqueue, conrel, childrel, pkrelid, + childtup, lockmode); + table_close(childrel, NoLock); + } - if (currcon->conenforced != cmdcon->is_enforced) - { - AlterConstrUpdateConstraintEntry(cmdcon, conrel, contuple); - changed = true; + systable_endscan(pscan); } - /* 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) - AlterConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, tgrel, - fkrelid, pkrelid, contuple, - lockmode, InvalidOid, InvalidOid, - InvalidOid, InvalidOid); + /* + * 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); - /* Drop all the triggers */ - DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid); - } - else if (changed) /* Create triggers */ - { - Oid ReferencedDelTriggerOid = InvalidOid, - ReferencedUpdTriggerOid = InvalidOid, - ReferencingInsTriggerOid = InvalidOid, - ReferencingUpdTriggerOid = InvalidOid; + InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0); - /* Prepare the minimal information required for trigger creation. */ - Constraint *fkconstraint = makeNode(Constraint); + heap_freetuple(copyTuple); + } - fkconstraint->conname = pstrdup(NameStr(currcon->conname)); - fkconstraint->fk_matchtype = currcon->confmatchtype; - fkconstraint->fk_upd_action = currcon->confupdtype; - fkconstraint->fk_del_action = currcon->confdeltype; + /* + * QueueCheckConstraintValidation + * + * Add an entry to the wqueue to validate the given check constraint in Phase 3 + * and update the convalidated field in the pg_constraint catalog for the + * specified relation and all its inheriting children. + */ + static void + QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel, + char *constrName, HeapTuple contuple, + bool recurse, bool recursing, LOCKMODE lockmode) + { + Form_pg_constraint con; + AlteredTableInfo *tab; + HeapTuple copyTuple; + Form_pg_constraint copy_con; - /* Create referenced triggers */ - if (currcon->conrelid == fkrelid) - createForeignKeyActionTriggers(currcon->conrelid, - currcon->confrelid, - fkconstraint, - conoid, - currcon->conindid, - ReferencedParentDelTrigger, - ReferencedParentUpdTrigger, - &ReferencedDelTriggerOid, - &ReferencedUpdTriggerOid); + List *children = NIL; + ListCell *child; + NewConstraint *newcon; + Datum val; + char *conbin; - /* Create referencing triggers */ - if (currcon->confrelid == pkrelid) - createForeignKeyCheckTriggers(currcon->conrelid, - pkrelid, - fkconstraint, - conoid, - currcon->conindid, - ReferencingParentInsTrigger, - ReferencingParentUpdTrigger, - &ReferencingInsTriggerOid, - &ReferencingUpdTriggerOid); + con = (Form_pg_constraint) GETSTRUCT(contuple); + Assert(con->contype == CONSTRAINT_CHECK); - /* - * 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; + /* + * If we're recursing, the parent has already done this, so skip it. Also, + * if the constraint is a NO INHERIT constraint, we shouldn't try to look + * for it in the children. + */ + if (!recursing && !con->connoinherit) + children = find_all_inheritors(RelationGetRelid(rel), + lockmode, NULL); ++<<<<<<< ours + 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; ++======= + /* + * For CHECK constraints, we must ensure that we only mark the constraint + * as validated on the parent if it's already validated on the children. + * + * We recurse before validating on the parent, to reduce risk of + * deadlocks. + */ + foreach(child, children) + { + Oid childoid = lfirst_oid(child); + Relation childrel; ++>>>>>>> theirs - /* Find or create work queue entry for this table */ - tab = ATGetQueueEntry(wqueue, rel); - tab->constraints = lappend(tab->constraints, newcon); - } + if (childoid == RelationGetRelid(rel)) + continue; /* - * 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 we are told not to recurse, there had better not be any child + * tables, because we can't mark the constraint on the parent valid + * unless it is valid for all child tables. */ - if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE || - get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE) - AlterConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, tgrel, - fkrelid, pkrelid, contuple, - lockmode, ReferencedDelTriggerOid, - ReferencedUpdTriggerOid, - ReferencingInsTriggerOid, - ReferencingUpdTriggerOid); - } - - table_close(rel, NoLock); - - return changed; - } + if (!recurse) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("constraint must be validated on child tables too"))); - /* - * 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.) - */ - static bool - ATExecAlterConstrDeferrability(List **wqueue, ATAlterConstraint *cmdcon, - Relation conrel, Relation tgrel, Relation rel, - HeapTuple contuple, bool recurse, - List **otherrelids, LOCKMODE lockmode) - { - Form_pg_constraint currcon; - Oid refrelid; - bool changed = false; + /* find_all_inheritors already got lock */ + childrel = table_open(childoid, NoLock); - /* since this function recurses, it could be driven to stack overflow */ - check_stack_depth(); + ATExecValidateConstraint(wqueue, childrel, constrName, false, + true, lockmode); + table_close(childrel, NoLock); + } - Assert(cmdcon->alterDeferrability); + /* Queue validation for phase 3 */ + newcon = (NewConstraint *) palloc0(sizeof(NewConstraint)); + newcon->name = constrName; + newcon->contype = CONSTR_CHECK; + newcon->refrelid = InvalidOid; + newcon->refindid = InvalidOid; + newcon->conid = con->oid; - currcon = (Form_pg_constraint) GETSTRUCT(contuple); - refrelid = currcon->confrelid; + val = SysCacheGetAttrNotNull(CONSTROID, contuple, + Anum_pg_constraint_conbin); + conbin = TextDatumGetCString(val); + newcon->qual = expand_generated_columns_in_expr(stringToNode(conbin), rel, 1); - /* Should be foreign key constraint */ - Assert(currcon->contype == CONSTRAINT_FOREIGN); + /* Find or create work queue entry for this table */ + tab = ATGetQueueEntry(wqueue, rel); + tab->constraints = lappend(tab->constraints, newcon); /* - * If called to modify a constraint that's already in the desired state, - * silently do nothing. + * Invalidate relcache so that others see the new validated constraint. */ - if (currcon->condeferrable != cmdcon->deferrable || - currcon->condeferred != cmdcon->initdeferred) - { - AlterConstrUpdateConstraintEntry(cmdcon, conrel, contuple); - changed = true; - - /* - * Now we need to update the multiple entries in pg_trigger that - * implement the constraint. - */ - AlterConstrTriggerDeferrability(currcon->oid, tgrel, rel, - cmdcon->deferrable, - cmdcon->initdeferred, otherrelids); - } + CacheInvalidateRelcache(rel); /* - * If the table at either end of the constraint is partitioned, we need to - * handle every constraint that is a child of this one. + * Now update the catalog, while we have the door open. */ - 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); + copyTuple = heap_copytuple(contuple); + copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); + copy_con->convalidated = true; + CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple); - return changed; + InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0); + + heap_freetuple(copyTuple); } /* @@@ -13018,328 -12362,318 +12410,391 @@@ transformFkeyGetPrimaryKey(Relation pkr } /* - * QueueFKConstraintValidation + * transformFkeyCheckAttrs - * - * 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. + * Validate that the 'attnums' columns in the 'pkrel' relation are valid to + * reference as part of a foreign key constraint. + * + * Returns the OID of the unique index supporting the constraint and + * populates the caller-provided 'opclasses' array with the opclasses + * associated with the index columns. Also sets whether the index + * uses WITHOUT OVERLAPS. + * + * Raises an ERROR on validation failure. */ - static void - QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation fkrel, - Oid pkrelid, HeapTuple contuple, LOCKMODE lockmode) + static Oid + transformFkeyCheckAttrs(Relation pkrel, + int numattrs, int16 *attnums, + bool with_period, Oid *opclasses, + bool *pk_has_without_overlaps) { - Form_pg_constraint con; - AlteredTableInfo *tab; - HeapTuple copyTuple; - Form_pg_constraint copy_con; - - con = (Form_pg_constraint) GETSTRUCT(contuple); - Assert(con->contype == CONSTRAINT_FOREIGN); - Assert(!con->convalidated); + Oid indexoid = InvalidOid; + bool found = false; + bool found_deferrable = false; + List *indexoidlist; + ListCell *indexoidscan; + int i, + j; /* - * 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. + * Reject duplicate appearances of columns in the referenced-columns list. + * Such a case is forbidden by the SQL standard, and even if we thought it + * useful to allow it, there would be ambiguity about how to match the + * list to unique indexes (in particular, it'd be unclear which index + * opclass goes with which FK column). */ - if (fkrel->rd_rel->relkind == RELKIND_RELATION && - con->confrelid == pkrelid) + for (i = 0; i < numattrs; i++) { ++<<<<<<< ours + NewConstraint *newcon; + Constraint *fkconstraint; + + /* Queue validation for phase 3 */ + fkconstraint = makeNode(Constraint); + /* for now this is all we need */ + fkconstraint->conname = pstrdup(NameStr(con->conname)); + + newcon = palloc0_object(NewConstraint); + newcon->name = fkconstraint->conname; + newcon->contype = CONSTR_FOREIGN; + newcon->refrelid = con->confrelid; + newcon->refindid = con->conindid; + newcon->conid = con->oid; + newcon->qual = (Node *) fkconstraint; + + /* Find or create work queue entry for this table */ + tab = ATGetQueueEntry(wqueue, fkrel); + tab->constraints = lappend(tab->constraints, newcon); + } + + /* + * If the table at either end of the constraint is partitioned, we need to + * recurse and handle every unvalidated constraint that is a child of this + * constraint. ++======= + for (j = i + 1; j < numattrs; j++) + { + if (attnums[i] == attnums[j]) + ereport(ERROR, + (errcode(ERRCODE_INVALID_FOREIGN_KEY), + errmsg("foreign key referenced-columns list must not contain duplicates"))); + } + } + + /* + * Get the list of index OIDs for the table from the relcache, and look up + * each one in the pg_index syscache, and match unique indexes to the list + * of attnums we are given. ++>>>>>>> theirs */ - if (fkrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE || - get_rel_relkind(con->confrelid) == RELKIND_PARTITIONED_TABLE) + indexoidlist = RelationGetIndexList(pkrel); + + foreach(indexoidscan, indexoidlist) { - ScanKeyData pkey; - SysScanDesc pscan; - HeapTuple childtup; + HeapTuple indexTuple; + Form_pg_index indexStruct; - ScanKeyInit(&pkey, - Anum_pg_constraint_conparentid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(con->oid)); - - pscan = systable_beginscan(conrel, ConstraintParentIndexId, - true, NULL, 1, &pkey); + indexoid = lfirst_oid(indexoidscan); + indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid)); + if (!HeapTupleIsValid(indexTuple)) + elog(ERROR, "cache lookup failed for index %u", indexoid); + indexStruct = (Form_pg_index) GETSTRUCT(indexTuple); - while (HeapTupleIsValid(childtup = systable_getnext(pscan))) + /* + * Must have the right number of columns; must be unique (or if + * temporal then exclusion instead) and not a partial index; forget it + * if there are any expressions, too. Invalid indexes are out as well. + */ + if (indexStruct->indnkeyatts == numattrs && + (with_period ? indexStruct->indisexclusion : indexStruct->indisunique) && + indexStruct->indisvalid && + heap_attisnull(indexTuple, Anum_pg_index_indpred, NULL) && + heap_attisnull(indexTuple, Anum_pg_index_indexprs, NULL)) { - Form_pg_constraint childcon; - Relation childrel; + Datum indclassDatum; + oidvector *indclass; - childcon = (Form_pg_constraint) GETSTRUCT(childtup); + /* Must get indclass the hard way */ + indclassDatum = SysCacheGetAttrNotNull(INDEXRELID, indexTuple, + Anum_pg_index_indclass); + indclass = (oidvector *) DatumGetPointer(indclassDatum); /* - * If the child constraint has already been validated, no further - * action is required for it or its descendants, as they are all - * valid. + * The given attnum list may match the index columns in any order. + * Check for a match, and extract the appropriate opclasses while + * we're at it. + * + * We know that attnums[] is duplicate-free per the test at the + * start of this function, and we checked above that the number of + * index columns agrees, so if we find a match for each attnums[] + * entry then we must have a one-to-one match in some order. */ - if (childcon->convalidated) - continue; + for (i = 0; i < numattrs; i++) + { + found = false; + for (j = 0; j < numattrs; j++) + { + if (attnums[i] == indexStruct->indkey.values[j]) + { + opclasses[i] = indclass->values[j]; + found = true; + break; + } + } + if (!found) + break; + } + /* The last attribute in the index must be the PERIOD FK part */ + if (found && with_period) + { + int16 periodattnum = attnums[numattrs - 1]; - childrel = table_open(childcon->conrelid, lockmode); + found = (periodattnum == indexStruct->indkey.values[numattrs - 1]); + } /* - * NB: Note that pkrelid should be passed as-is during recursion, - * as it is required to identify the root referenced table. + * Refuse to use a deferrable unique/primary key. This is per SQL + * spec, and there would be a lot of interesting semantic problems + * if we tried to allow it. */ - QueueFKConstraintValidation(wqueue, conrel, childrel, pkrelid, - childtup, lockmode); - table_close(childrel, NoLock); + if (found && !indexStruct->indimmediate) + { + /* + * Remember that we found an otherwise matching index, so that + * we can generate a more appropriate error message. + */ + found_deferrable = true; + found = false; + } + + /* We need to know whether the index has WITHOUT OVERLAPS */ + if (found) + *pk_has_without_overlaps = indexStruct->indisexclusion; } + ReleaseSysCache(indexTuple); + if (found) + break; + } - systable_endscan(pscan); + if (!found) + { + if (found_deferrable) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot use a deferrable unique constraint for referenced table \"%s\"", + RelationGetRelationName(pkrel)))); + else + ereport(ERROR, + (errcode(ERRCODE_INVALID_FOREIGN_KEY), + errmsg("there is no unique constraint matching given keys for referenced table \"%s\"", + RelationGetRelationName(pkrel)))); } - /* - * 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); + list_free(indexoidlist); - InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0); + return indexoid; + } - heap_freetuple(copyTuple); + /* + * findFkeyCast - + * + * Wrapper around find_coercion_pathway() for ATAddForeignKeyConstraint(). + * Caller has equal regard for binary coercibility and for an exact match. + */ + static CoercionPathType + findFkeyCast(Oid targetTypeId, Oid sourceTypeId, Oid *funcid) + { + CoercionPathType ret; + + if (targetTypeId == sourceTypeId) + { + ret = COERCION_PATH_RELABELTYPE; + *funcid = InvalidOid; + } + else + { + ret = find_coercion_pathway(targetTypeId, sourceTypeId, + COERCION_IMPLICIT, funcid); + if (ret == COERCION_PATH_NONE) + /* A previously-relied-upon cast is now gone. */ + elog(ERROR, "could not find cast from %u to %u", + sourceTypeId, targetTypeId); + } + + return ret; } /* - * QueueCheckConstraintValidation + * Permissions checks on the referenced table for ADD FOREIGN KEY * - * Add an entry to the wqueue to validate the given check constraint in Phase 3 - * and update the convalidated field in the pg_constraint catalog for the - * specified relation and all its inheriting children. + * Note: we have already checked that the user owns the referencing table, + * else we'd have failed much earlier; no additional checks are needed for it. */ static void - QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel, - char *constrName, HeapTuple contuple, - bool recurse, bool recursing, LOCKMODE lockmode) + checkFkeyPermissions(Relation rel, int16 *attnums, int natts) { - Form_pg_constraint con; - AlteredTableInfo *tab; - HeapTuple copyTuple; - Form_pg_constraint copy_con; + Oid roleid = GetUserId(); + AclResult aclresult; + int i; - List *children = NIL; - ListCell *child; - NewConstraint *newcon; - Datum val; - char *conbin; + /* Okay if we have relation-level REFERENCES permission */ + aclresult = pg_class_aclcheck(RelationGetRelid(rel), roleid, + ACL_REFERENCES); + if (aclresult == ACLCHECK_OK) + return; + /* Else we must have REFERENCES on each column */ + for (i = 0; i < natts; i++) + { + aclresult = pg_attribute_aclcheck(RelationGetRelid(rel), attnums[i], + roleid, ACL_REFERENCES); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind), + RelationGetRelationName(rel)); + } + } - con = (Form_pg_constraint) GETSTRUCT(contuple); - Assert(con->contype == CONSTRAINT_CHECK); + /* + * Scan the existing rows in a table to verify they meet a proposed FK + * constraint. + * + * Caller must have opened and locked both relations appropriately. + */ + static void + validateForeignKeyConstraint(char *conname, + Relation rel, + Relation pkrel, + Oid pkindOid, + Oid constraintOid, + bool hasperiod) + { + TupleTableSlot *slot; + TableScanDesc scan; + Trigger trig = {0}; + Snapshot snapshot; + MemoryContext oldcxt; + MemoryContext perTupCxt; - /* - * If we're recursing, the parent has already done this, so skip it. Also, - * if the constraint is a NO INHERIT constraint, we shouldn't try to look - * for it in the children. - */ - if (!recursing && !con->connoinherit) - children = find_all_inheritors(RelationGetRelid(rel), - lockmode, NULL); + ereport(DEBUG1, + (errmsg_internal("validating foreign key constraint \"%s\"", conname))); /* - * For CHECK constraints, we must ensure that we only mark the constraint - * as validated on the parent if it's already validated on the children. - * - * We recurse before validating on the parent, to reduce risk of - * deadlocks. + * Build a trigger call structure; we'll need it either way. */ ++<<<<<<< ours + foreach(child, children) + { + Oid childoid = lfirst_oid(child); + Relation childrel; + + if (childoid == RelationGetRelid(rel)) + continue; + + /* + * If we are told not to recurse, there had better not be any child + * tables, because we can't mark the constraint on the parent valid + * unless it is valid for all child tables. + */ + if (!recurse) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("constraint must be validated on child tables too"))); + + /* find_all_inheritors already got lock */ + childrel = table_open(childoid, NoLock); + + ATExecValidateConstraint(wqueue, childrel, constrName, false, + true, lockmode); + table_close(childrel, NoLock); + } + + /* Queue validation for phase 3 */ + newcon = palloc0_object(NewConstraint); + newcon->name = constrName; + newcon->contype = CONSTR_CHECK; + newcon->refrelid = InvalidOid; + newcon->refindid = InvalidOid; + newcon->conid = con->oid; + + val = SysCacheGetAttrNotNull(CONSTROID, contuple, + Anum_pg_constraint_conbin); + conbin = TextDatumGetCString(val); + newcon->qual = expand_generated_columns_in_expr(stringToNode(conbin), rel, 1); + + /* Find or create work queue entry for this table */ + tab = ATGetQueueEntry(wqueue, rel); + tab->constraints = lappend(tab->constraints, newcon); ++======= + trig.tgoid = InvalidOid; + trig.tgname = conname; + trig.tgenabled = TRIGGER_FIRES_ON_ORIGIN; + trig.tgisinternal = true; + trig.tgconstrrelid = RelationGetRelid(pkrel); + trig.tgconstrindid = pkindOid; + trig.tgconstraint = constraintOid; + trig.tgdeferrable = false; + trig.tginitdeferred = false; + /* we needn't fill in remaining fields */ ++>>>>>>> theirs /* - * Invalidate relcache so that others see the new validated constraint. + * See if we can do it with a single LEFT JOIN query. A false result + * indicates we must proceed with the fire-the-trigger method. We can't do + * a LEFT JOIN for temporal FKs yet, but we can once we support temporal + * left joins. */ - CacheInvalidateRelcache(rel); + if (!hasperiod && RI_Initial_Check(&trig, rel, pkrel)) + return; /* - * Now update the catalog, while we have the door open. + * Scan through each tuple, calling RI_FKey_check_ins (insert trigger) as + * if that tuple had just been inserted. If any of those fail, it should + * ereport(ERROR) and that's that. */ - copyTuple = heap_copytuple(contuple); - copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); - copy_con->convalidated = true; - CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple); + snapshot = RegisterSnapshot(GetLatestSnapshot()); + slot = table_slot_create(rel, NULL); + scan = table_beginscan(rel, snapshot, 0, NULL); - InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0); + perTupCxt = AllocSetContextCreate(CurrentMemoryContext, + "validateForeignKeyConstraint", + ALLOCSET_SMALL_SIZES); + oldcxt = MemoryContextSwitchTo(perTupCxt); - heap_freetuple(copyTuple); - } - - /* - * QueueNNConstraintValidation - * - * Add an entry to the wqueue to validate the given not-null constraint in - * Phase 3 and update the convalidated field in the pg_constraint catalog for - * the specified relation and all its inheriting children. - */ - static void - QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel, - HeapTuple contuple, bool recurse, bool recursing, - LOCKMODE lockmode) - { - Form_pg_constraint con; - AlteredTableInfo *tab; - HeapTuple copyTuple; - Form_pg_constraint copy_con; - List *children = NIL; - AttrNumber attnum; - char *colname; - - con = (Form_pg_constraint) GETSTRUCT(contuple); - Assert(con->contype == CONSTRAINT_NOTNULL); - - attnum = extractNotNullColumn(contuple); - - /* - * If we're recursing, we've already done this for parent, so skip it. - * Also, if the constraint is a NO INHERIT constraint, we shouldn't try to - * look for it in the children. - * - * We recurse before validating on the parent, to reduce risk of - * deadlocks. - */ - if (!recursing && !con->connoinherit) - children = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL); - - colname = get_attname(RelationGetRelid(rel), attnum, false); - foreach_oid(childoid, children) + while (table_scan_getnextslot(scan, ForwardScanDirection, slot)) { - Relation childrel; - HeapTuple contup; - Form_pg_constraint childcon; - char *conname; + LOCAL_FCINFO(fcinfo, 0); + TriggerData trigdata = {0}; - if (childoid == RelationGetRelid(rel)) - continue; + CHECK_FOR_INTERRUPTS(); /* - * If we are told not to recurse, there had better not be any child - * tables, because we can't mark the constraint on the parent valid - * unless it is valid for all child tables. + * Make a call to the trigger function + * + * No parameters are passed, but we do set a context */ - if (!recurse) - ereport(ERROR, - errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("constraint must be validated on child tables too")); + MemSet(fcinfo, 0, SizeForFunctionCallInfo(0)); /* - * The column on child might have a different attnum, so search by - * column name. + * We assume RI_FKey_check_ins won't look at flinfo... */ - contup = findNotNullConstraint(childoid, colname); - if (!contup) - elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"", - colname, get_rel_name(childoid)); - childcon = (Form_pg_constraint) GETSTRUCT(contup); - if (childcon->convalidated) - continue; - - /* find_all_inheritors already got lock */ - childrel = table_open(childoid, NoLock); - conname = pstrdup(NameStr(childcon->conname)); - - /* XXX improve ATExecValidateConstraint API to avoid double search */ - ATExecValidateConstraint(wqueue, childrel, conname, - false, true, lockmode); - table_close(childrel, NoLock); - } - - /* Set attnotnull appropriately without queueing another validation */ - set_attnotnull(NULL, rel, attnum, true, false); - - tab = ATGetQueueEntry(wqueue, rel); - tab->verify_new_notnull = true; + trigdata.type = T_TriggerData; + trigdata.tg_event = TRIGGER_EVENT_INSERT | TRIGGER_EVENT_ROW; + trigdata.tg_relation = rel; + trigdata.tg_trigtuple = ExecFetchSlotHeapTuple(slot, false, NULL); + trigdata.tg_trigslot = slot; + trigdata.tg_trigger = &trig; - /* - * Invalidate relcache so that others see the new validated constraint. - */ - CacheInvalidateRelcache(rel); + fcinfo->context = (Node *) &trigdata; - /* - * Now update the catalogs, while we have the door open. - */ - copyTuple = heap_copytuple(contuple); - copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); - copy_con->convalidated = true; - CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple); + RI_FKey_check_ins(fcinfo); - InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0); + MemoryContextReset(perTupCxt); + } - heap_freetuple(copyTuple); + MemoryContextSwitchTo(oldcxt); + MemoryContextDelete(perTupCxt); + table_endscan(scan); + UnregisterSnapshot(snapshot); + ExecDropSingleTupleTableSlot(slot); } /* @@@ -13657,8377 -12959,4599 +13080,7879 @@@ ATExecDropConstraint(Relation rel, cons } /* - * findFkeyCast - + * Remove a constraint, using its pg_constraint tuple * - * Wrapper around find_coercion_pathway() for ATAddForeignKeyConstraint(). - * Caller has equal regard for binary coercibility and for an exact match. - */ - static CoercionPathType - findFkeyCast(Oid targetTypeId, Oid sourceTypeId, Oid *funcid) - { - CoercionPathType ret; - - if (targetTypeId == sourceTypeId) - { - ret = COERCION_PATH_RELABELTYPE; - *funcid = InvalidOid; - } - else - { - ret = find_coercion_pathway(targetTypeId, sourceTypeId, - COERCION_IMPLICIT, funcid); - if (ret == COERCION_PATH_NONE) - /* A previously-relied-upon cast is now gone. */ - elog(ERROR, "could not find cast from %u to %u", - sourceTypeId, targetTypeId); - } - - return ret; - } - - /* - * Permissions checks on the referenced table for ADD FOREIGN KEY + * Implementation for ALTER TABLE DROP CONSTRAINT and ALTER TABLE ALTER COLUMN + * DROP NOT NULL. * - * Note: we have already checked that the user owns the referencing table, - * else we'd have failed much earlier; no additional checks are needed for it. + * Returns the address of the constraint being removed. */ - static void - checkFkeyPermissions(Relation rel, int16 *attnums, int natts) + static ObjectAddress + dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior behavior, + bool recurse, bool recursing, bool missing_ok, + LOCKMODE lockmode) { - Oid roleid = GetUserId(); - AclResult aclresult; - int i; + Relation conrel; + Form_pg_constraint con; + ObjectAddress conobj; + List *children; + bool is_no_inherit_constraint = false; + char *constrName; + char *colname = NULL; - /* Okay if we have relation-level REFERENCES permission */ - aclresult = pg_class_aclcheck(RelationGetRelid(rel), roleid, - ACL_REFERENCES); - if (aclresult == ACLCHECK_OK) - return; - /* Else we must have REFERENCES on each column */ - for (i = 0; i < natts; i++) - { - aclresult = pg_attribute_aclcheck(RelationGetRelid(rel), attnums[i], - roleid, ACL_REFERENCES); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind), - RelationGetRelationName(rel)); - } - } + /* Guard against stack overflow due to overly deep inheritance tree. */ + check_stack_depth(); - /* - * Scan the existing rows in a table to verify they meet a proposed FK - * constraint. - * - * Caller must have opened and locked both relations appropriately. - */ - static void - validateForeignKeyConstraint(char *conname, - Relation rel, - Relation pkrel, - Oid pkindOid, - Oid constraintOid, - bool hasperiod) - { - TupleTableSlot *slot; - TableScanDesc scan; - Trigger trig = {0}; - Snapshot snapshot; - MemoryContext oldcxt; - MemoryContext perTupCxt; + /* At top level, permission check was done in ATPrepCmd, else do it */ + if (recursing) + ATSimplePermissions(AT_DropConstraint, rel, + ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); - ereport(DEBUG1, - (errmsg_internal("validating foreign key constraint \"%s\"", conname))); + conrel = table_open(ConstraintRelationId, RowExclusiveLock); - /* - * Build a trigger call structure; we'll need it either way. - */ - trig.tgoid = InvalidOid; - trig.tgname = conname; - trig.tgenabled = TRIGGER_FIRES_ON_ORIGIN; - trig.tgisinternal = true; - trig.tgconstrrelid = RelationGetRelid(pkrel); - trig.tgconstrindid = pkindOid; - trig.tgconstraint = constraintOid; - trig.tgdeferrable = false; - trig.tginitdeferred = false; - /* we needn't fill in remaining fields */ + con = (Form_pg_constraint) GETSTRUCT(constraintTup); + constrName = NameStr(con->conname); - /* - * See if we can do it with a single LEFT JOIN query. A false result - * indicates we must proceed with the fire-the-trigger method. We can't do - * a LEFT JOIN for temporal FKs yet, but we can once we support temporal - * left joins. - */ - if (!hasperiod && RI_Initial_Check(&trig, rel, pkrel)) - return; + /* Don't allow drop of inherited constraints */ + if (con->coninhcount > 0 && !recursing) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("cannot drop inherited constraint \"%s\" of relation \"%s\"", + constrName, RelationGetRelationName(rel)))); /* - * Scan through each tuple, calling RI_FKey_check_ins (insert trigger) as - * if that tuple had just been inserted. If any of those fail, it should - * ereport(ERROR) and that's that. + * Reset pg_constraint.attnotnull, if this is a not-null constraint. + * + * While doing that, we're in a good position to disallow dropping a not- + * null constraint underneath a primary key, a replica identity index, or + * a generated identity column. */ - snapshot = RegisterSnapshot(GetLatestSnapshot()); - slot = table_slot_create(rel, NULL); - scan = table_beginscan(rel, snapshot, 0, NULL); - - perTupCxt = AllocSetContextCreate(CurrentMemoryContext, - "validateForeignKeyConstraint", - ALLOCSET_SMALL_SIZES); - oldcxt = MemoryContextSwitchTo(perTupCxt); - - while (table_scan_getnextslot(scan, ForwardScanDirection, slot)) + if (con->contype == CONSTRAINT_NOTNULL) { - LOCAL_FCINFO(fcinfo, 0); - TriggerData trigdata = {0}; + Relation attrel = table_open(AttributeRelationId, RowExclusiveLock); + AttrNumber attnum = extractNotNullColumn(constraintTup); + Bitmapset *pkattrs; + Bitmapset *irattrs; + HeapTuple atttup; + Form_pg_attribute attForm; - CHECK_FOR_INTERRUPTS(); + /* save column name for recursion step */ + colname = get_attname(RelationGetRelid(rel), attnum, false); /* - * Make a call to the trigger function - * - * No parameters are passed, but we do set a context + * Disallow if it's in the primary key. For partitioned tables we + * cannot rely solely on RelationGetIndexAttrBitmap, because it'll + * return NULL if the primary key is invalid; but we still need to + * protect not-null constraints under such a constraint, so check the + * slow way. */ - MemSet(fcinfo, 0, SizeForFunctionCallInfo(0)); + pkattrs = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_PRIMARY_KEY); - /* - * We assume RI_FKey_check_ins won't look at flinfo... - */ - trigdata.type = T_TriggerData; - trigdata.tg_event = TRIGGER_EVENT_INSERT | TRIGGER_EVENT_ROW; - trigdata.tg_relation = rel; - trigdata.tg_trigtuple = ExecFetchSlotHeapTuple(slot, false, NULL); - trigdata.tg_trigslot = slot; - trigdata.tg_trigger = &trig; + if (pkattrs == NULL && + rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { + Oid pkindex = RelationGetPrimaryKeyIndex(rel, true); - fcinfo->context = (Node *) &trigdata; + if (OidIsValid(pkindex)) + { + Relation pk = relation_open(pkindex, AccessShareLock); - RI_FKey_check_ins(fcinfo); + pkattrs = NULL; + for (int i = 0; i < pk->rd_index->indnkeyatts; i++) + pkattrs = bms_add_member(pkattrs, pk->rd_index->indkey.values[i]); - MemoryContextReset(perTupCxt); - } + relation_close(pk, AccessShareLock); + } + } - MemoryContextSwitchTo(oldcxt); - MemoryContextDelete(perTupCxt); - table_endscan(scan); - UnregisterSnapshot(snapshot); - ExecDropSingleTupleTableSlot(slot); - } + if (pkattrs && + bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, pkattrs)) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("column \"%s\" is in a primary key", + get_attname(RelationGetRelid(rel), attnum, false))); - /* - * CreateFKCheckTrigger - * Creates the insert (on_insert=true) or update "check" trigger that - * implements a given foreign key - * - * Returns the OID of the so created trigger. - */ - static Oid - CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint, - Oid constraintOid, Oid indexOid, Oid parentTrigOid, - bool on_insert) - { - ObjectAddress trigAddress; - CreateTrigStmt *fk_trigger; + /* Disallow if it's in the replica identity */ + irattrs = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_IDENTITY_KEY); + if (bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, irattrs)) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("column \"%s\" is in index used as replica identity", + get_attname(RelationGetRelid(rel), attnum, false))); + + /* Disallow if it's a GENERATED AS IDENTITY column */ + atttup = SearchSysCacheCopyAttNum(RelationGetRelid(rel), attnum); + if (!HeapTupleIsValid(atttup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + attnum, RelationGetRelid(rel)); + attForm = (Form_pg_attribute) GETSTRUCT(atttup); + if (attForm->attidentity != '\0') + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("column \"%s\" of relation \"%s\" is an identity column", + get_attname(RelationGetRelid(rel), attnum, + false), + RelationGetRelationName(rel))); + + /* All good -- reset attnotnull if needed */ + if (attForm->attnotnull) + { + attForm->attnotnull = false; + CatalogTupleUpdate(attrel, &atttup->t_self, atttup); + } + + table_close(attrel, RowExclusiveLock); + } + + is_no_inherit_constraint = con->connoinherit; /* - * Note: for a self-referential FK (referencing and referenced tables are - * the same), it is important that the ON UPDATE action fires before the - * CHECK action, since both triggers will fire on the same row during an - * UPDATE event; otherwise the CHECK trigger will be checking a non-final - * state of the row. Triggers fire in name order, so we ensure this by - * using names like "RI_ConstraintTrigger_a_NNNN" for the action triggers - * and "RI_ConstraintTrigger_c_NNNN" for the check triggers. + * If it's a foreign-key constraint, we'd better lock the referenced table + * and check that that's not in use, just as we've already done for the + * constrained table (else we might, eg, be dropping a trigger that has + * unfired events). But we can/must skip that in the self-referential + * case. */ - fk_trigger = makeNode(CreateTrigStmt); - fk_trigger->replace = false; - fk_trigger->isconstraint = true; - fk_trigger->trigname = "RI_ConstraintTrigger_c"; - fk_trigger->relation = NULL; - - /* Either ON INSERT or ON UPDATE */ - if (on_insert) + if (con->contype == CONSTRAINT_FOREIGN && + con->confrelid != RelationGetRelid(rel)) { - fk_trigger->funcname = SystemFuncName("RI_FKey_check_ins"); - fk_trigger->events = TRIGGER_TYPE_INSERT; + Relation frel; + + /* Must match lock taken by RemoveTriggerById: */ + frel = table_open(con->confrelid, AccessExclusiveLock); + CheckAlterTableIsSafe(frel); + table_close(frel, NoLock); } - else + + /* + * Perform the actual constraint deletion + */ + ObjectAddressSet(conobj, ConstraintRelationId, con->oid); + performDeletion(&conobj, behavior, 0); + + /* + * For partitioned tables, non-CHECK, non-NOT-NULL inherited constraints + * are dropped via the dependency mechanism, so we're done here. + */ + if (con->contype != CONSTRAINT_CHECK && + con->contype != CONSTRAINT_NOTNULL && + rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) { - fk_trigger->funcname = SystemFuncName("RI_FKey_check_upd"); - fk_trigger->events = TRIGGER_TYPE_UPDATE; + table_close(conrel, RowExclusiveLock); + return conobj; } - fk_trigger->args = NIL; - fk_trigger->row = true; - fk_trigger->timing = TRIGGER_TYPE_AFTER; - fk_trigger->columns = NIL; - fk_trigger->whenClause = NULL; - fk_trigger->transitionRels = NIL; - fk_trigger->deferrable = fkconstraint->deferrable; - fk_trigger->initdeferred = fkconstraint->initdeferred; - fk_trigger->constrrel = NULL; + /* + * Propagate to children as appropriate. Unlike most other ALTER + * routines, we have to do this one level of recursion at a time; we can't + * use find_all_inheritors to do it in one pass. + */ + if (!is_no_inherit_constraint) + children = find_inheritance_children(RelationGetRelid(rel), lockmode); + else + children = NIL; - trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid, - constraintOid, indexOid, InvalidOid, - parentTrigOid, NULL, true, false); + foreach_oid(childrelid, children) + { + Relation childrel; + HeapTuple tuple; + Form_pg_constraint childcon; - /* Make changes-so-far visible */ - CommandCounterIncrement(); + /* find_inheritance_children already got lock */ + childrel = table_open(childrelid, NoLock); + CheckAlterTableIsSafe(childrel); - /* - * We search for not-null constraints by column name, and others by - * constraint name. - */ - if (con->contype == CONSTRAINT_NOTNULL) - { - tuple = findNotNullConstraint(childrelid, colname); - if (!HeapTupleIsValid(tuple)) - elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u", - colname, RelationGetRelid(childrel)); - } - else - { - SysScanDesc scan; - ScanKeyData skey[3]; - - ScanKeyInit(&skey[0], - Anum_pg_constraint_conrelid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(childrelid)); - ScanKeyInit(&skey[1], - Anum_pg_constraint_contypid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(InvalidOid)); - ScanKeyInit(&skey[2], - Anum_pg_constraint_conname, - BTEqualStrategyNumber, F_NAMEEQ, - CStringGetDatum(constrName)); - scan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId, - true, NULL, 3, skey); - /* There can only be one, so no need to loop */ - tuple = systable_getnext(scan); - if (!HeapTupleIsValid(tuple)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("constraint \"%s\" of relation \"%s\" does not exist", - constrName, - RelationGetRelationName(childrel)))); - tuple = heap_copytuple(tuple); - systable_endscan(scan); - } - - childcon = (Form_pg_constraint) GETSTRUCT(tuple); - - /* Right now only CHECK and not-null constraints can be inherited */ - if (childcon->contype != CONSTRAINT_CHECK && - childcon->contype != CONSTRAINT_NOTNULL) - elog(ERROR, "inherited constraint is not a CHECK or not-null constraint"); - - if (childcon->coninhcount <= 0) /* shouldn't happen */ - elog(ERROR, "relation %u has non-inherited constraint \"%s\"", - childrelid, NameStr(childcon->conname)); - - if (recurse) - { - /* - * If the child constraint has other definition sources, just - * decrement its inheritance count; if not, recurse to delete it. - */ - if (childcon->coninhcount == 1 && !childcon->conislocal) - { - /* Time to delete this child constraint, too */ - dropconstraint_internal(childrel, tuple, behavior, - recurse, true, missing_ok, - lockmode); - } - else - { - /* Child constraint must survive my deletion */ - childcon->coninhcount--; - CatalogTupleUpdate(conrel, &tuple->t_self, tuple); - - /* Make update visible */ - CommandCounterIncrement(); - } - } - else - { - /* - * If we were told to drop ONLY in this table (no recursion) and - * there are no further parents for this constraint, we need to - * mark the inheritors' constraints as locally defined rather than - * inherited. - */ - childcon->coninhcount--; - if (childcon->coninhcount == 0) - childcon->conislocal = true; - - CatalogTupleUpdate(conrel, &tuple->t_self, tuple); - - /* Make update visible */ - CommandCounterIncrement(); - } - - heap_freetuple(tuple); - - table_close(childrel, NoLock); - } - - table_close(conrel, RowExclusiveLock); - - return conobj; ++<<<<<<< ours + return trigAddress.objectId; } /* - * ALTER COLUMN TYPE - * - * Unlike other subcommand types, we do parse transformation for ALTER COLUMN - * TYPE during phase 1 --- the AlterTableCmd passed in here is already - * transformed (and must be, because we rely on some transformed fields). - * - * The point of this is that the execution of all ALTER COLUMN TYPEs for a - * table will be done "in parallel" during phase 3, so all the USING - * expressions should be parsed assuming the original column types. Also, - * this allows a USING expression to refer to a field that will be dropped. + * createForeignKeyActionTriggers + * Create the referenced-side "action" triggers that implement a foreign + * key. * - * To make this work safely, AT_PASS_DROP then AT_PASS_ALTER_TYPE must be - * the first two execution steps in phase 2; they must not see the effects - * of any other subcommand types, since the USING expressions are parsed - * against the unmodified table's state. + * Returns the OIDs of the so created triggers in *deleteTrigOid and + * *updateTrigOid. */ static void -ATPrepAlterColumnType(List **wqueue, - AlteredTableInfo *tab, Relation rel, - bool recurse, bool recursing, - AlterTableCmd *cmd, LOCKMODE lockmode, - AlterTableUtilityContext *context) +createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint, + Oid constraintOid, Oid indexOid, + Oid parentDelTrigger, Oid parentUpdTrigger, + Oid *deleteTrigOid, Oid *updateTrigOid) { - char *colName = cmd->name; - ColumnDef *def = (ColumnDef *) cmd->def; - TypeName *typeName = def->typeName; - Node *transform = def->cooked_default; - HeapTuple tuple; - Form_pg_attribute attTup; - AttrNumber attnum; - Oid targettype; - int32 targettypmod; - Oid targetcollid; - NewColumnValue *newval; - ParseState *pstate = make_parsestate(NULL); - AclResult aclresult; - bool is_expr; + CreateTrigStmt *fk_trigger; + ObjectAddress trigAddress; - pstate->p_sourcetext = context->queryString; + /* + * Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON + * DELETE action on the referenced table. + */ + fk_trigger = makeNode(CreateTrigStmt); + fk_trigger->replace = false; + fk_trigger->isconstraint = true; + fk_trigger->trigname = "RI_ConstraintTrigger_a"; + fk_trigger->relation = NULL; + fk_trigger->args = NIL; + fk_trigger->row = true; + fk_trigger->timing = TRIGGER_TYPE_AFTER; + fk_trigger->events = TRIGGER_TYPE_DELETE; + fk_trigger->columns = NIL; + fk_trigger->whenClause = NULL; + fk_trigger->transitionRels = NIL; + fk_trigger->constrrel = NULL; - if (rel->rd_rel->reloftype && !recursing) + switch (fkconstraint->fk_del_action) + { + case FKCONSTR_ACTION_NOACTION: + fk_trigger->deferrable = fkconstraint->deferrable; + fk_trigger->initdeferred = fkconstraint->initdeferred; + fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_del"); + break; + case FKCONSTR_ACTION_RESTRICT: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_del"); + break; + case FKCONSTR_ACTION_CASCADE: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_del"); + break; + case FKCONSTR_ACTION_SETNULL: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_del"); + break; + case FKCONSTR_ACTION_SETDEFAULT: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_del"); + break; + default: + elog(ERROR, "unrecognized FK action type: %d", + (int) fkconstraint->fk_del_action); + break; + } + + trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid, + constraintOid, indexOid, InvalidOid, + parentDelTrigger, NULL, true, false); + if (deleteTrigOid) + *deleteTrigOid = trigAddress.objectId; + + /* Make changes-so-far visible */ + CommandCounterIncrement(); + + /* + * Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON + * UPDATE action on the referenced table. + */ + fk_trigger = makeNode(CreateTrigStmt); + fk_trigger->replace = false; + fk_trigger->isconstraint = true; + fk_trigger->trigname = "RI_ConstraintTrigger_a"; + fk_trigger->relation = NULL; + fk_trigger->args = NIL; + fk_trigger->row = true; + fk_trigger->timing = TRIGGER_TYPE_AFTER; + fk_trigger->events = TRIGGER_TYPE_UPDATE; + fk_trigger->columns = NIL; + fk_trigger->whenClause = NULL; + fk_trigger->transitionRels = NIL; + fk_trigger->constrrel = NULL; + + switch (fkconstraint->fk_upd_action) + { + case FKCONSTR_ACTION_NOACTION: + fk_trigger->deferrable = fkconstraint->deferrable; + fk_trigger->initdeferred = fkconstraint->initdeferred; + fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_upd"); + break; + case FKCONSTR_ACTION_RESTRICT: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_upd"); + break; + case FKCONSTR_ACTION_CASCADE: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_upd"); + break; + case FKCONSTR_ACTION_SETNULL: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_upd"); + break; + case FKCONSTR_ACTION_SETDEFAULT: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_upd"); + break; + default: + elog(ERROR, "unrecognized FK action type: %d", + (int) fkconstraint->fk_upd_action); + break; + } + + trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid, + constraintOid, indexOid, InvalidOid, + parentUpdTrigger, NULL, true, false); + if (updateTrigOid) + *updateTrigOid = trigAddress.objectId; +} + +/* + * createForeignKeyCheckTriggers + * Create the referencing-side "check" triggers that implement a foreign + * key. + * + * Returns the OIDs of the so created triggers in *insertTrigOid and + * *updateTrigOid. + */ +static void +createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid, + Constraint *fkconstraint, Oid constraintOid, + Oid indexOid, + Oid parentInsTrigger, Oid parentUpdTrigger, + Oid *insertTrigOid, Oid *updateTrigOid) +{ + *insertTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, + constraintOid, indexOid, + parentInsTrigger, true); + *updateTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, + constraintOid, indexOid, + parentUpdTrigger, false); +} + +/* + * ALTER TABLE DROP CONSTRAINT + * + * Like DROP COLUMN, we can't use the normal ALTER TABLE recursion mechanism. + */ +static void +ATExecDropConstraint(Relation rel, const char *constrName, + DropBehavior behavior, bool recurse, + bool missing_ok, LOCKMODE lockmode) +{ + Relation conrel; + SysScanDesc scan; + ScanKeyData skey[3]; + HeapTuple tuple; + bool found = false; + + conrel = table_open(ConstraintRelationId, RowExclusiveLock); + + /* + * Find and drop the target constraint + */ + ScanKeyInit(&skey[0], + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(rel))); + ScanKeyInit(&skey[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + ScanKeyInit(&skey[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(constrName)); + scan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId, + true, NULL, 3, skey); + + /* There can be at most one matching row */ + if (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + dropconstraint_internal(rel, tuple, behavior, recurse, false, + missing_ok, lockmode); + found = true; + } + + systable_endscan(scan); + + if (!found) + { + if (!missing_ok) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("constraint \"%s\" of relation \"%s\" does not exist", + constrName, RelationGetRelationName(rel))); + else + ereport(NOTICE, + errmsg("constraint \"%s\" of relation \"%s\" does not exist, skipping", + constrName, RelationGetRelationName(rel))); + } + + table_close(conrel, RowExclusiveLock); +} + +/* + * Remove a constraint, using its pg_constraint tuple + * + * Implementation for ALTER TABLE DROP CONSTRAINT and ALTER TABLE ALTER COLUMN + * DROP NOT NULL. + * + * Returns the address of the constraint being removed. + */ +static ObjectAddress +dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior behavior, + bool recurse, bool recursing, bool missing_ok, + LOCKMODE lockmode) +{ + Relation conrel; + Form_pg_constraint con; + ObjectAddress conobj; + List *children; + bool is_no_inherit_constraint = false; + char *constrName; + char *colname = NULL; + + /* Guard against stack overflow due to overly deep inheritance tree. */ + check_stack_depth(); + + /* At top level, permission check was done in ATPrepCmd, else do it */ + if (recursing) + ATSimplePermissions(AT_DropConstraint, rel, + ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); + + conrel = table_open(ConstraintRelationId, RowExclusiveLock); + + con = (Form_pg_constraint) GETSTRUCT(constraintTup); + constrName = NameStr(con->conname); + + /* Don't allow drop of inherited constraints */ + if (con->coninhcount > 0 && !recursing) ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot alter column type of typed table"), - parser_errposition(pstate, def->location))); + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("cannot drop inherited constraint \"%s\" of relation \"%s\"", + constrName, RelationGetRelationName(rel)))); + + /* + * Reset pg_constraint.attnotnull, if this is a not-null constraint. + * + * While doing that, we're in a good position to disallow dropping a not- + * null constraint underneath a primary key, a replica identity index, or + * a generated identity column. + */ + if (con->contype == CONSTRAINT_NOTNULL) + { + Relation attrel = table_open(AttributeRelationId, RowExclusiveLock); + AttrNumber attnum = extractNotNullColumn(constraintTup); + Bitmapset *pkattrs; + Bitmapset *irattrs; + HeapTuple atttup; + Form_pg_attribute attForm; + + /* save column name for recursion step */ + colname = get_attname(RelationGetRelid(rel), attnum, false); + + /* + * Disallow if it's in the primary key. For partitioned tables we + * cannot rely solely on RelationGetIndexAttrBitmap, because it'll + * return NULL if the primary key is invalid; but we still need to + * protect not-null constraints under such a constraint, so check the + * slow way. + */ + pkattrs = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_PRIMARY_KEY); + + if (pkattrs == NULL && + rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { + Oid pkindex = RelationGetPrimaryKeyIndex(rel, true); + + if (OidIsValid(pkindex)) + { + Relation pk = relation_open(pkindex, AccessShareLock); + + pkattrs = NULL; + for (int i = 0; i < pk->rd_index->indnkeyatts; i++) + pkattrs = bms_add_member(pkattrs, pk->rd_index->indkey.values[i]); + + relation_close(pk, AccessShareLock); + } + } + + if (pkattrs && + bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, pkattrs)) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("column \"%s\" is in a primary key", + get_attname(RelationGetRelid(rel), attnum, false))); + + /* Disallow if it's in the replica identity */ + irattrs = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_IDENTITY_KEY); + if (bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, irattrs)) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("column \"%s\" is in index used as replica identity", + get_attname(RelationGetRelid(rel), attnum, false))); + + /* Disallow if it's a GENERATED AS IDENTITY column */ + atttup = SearchSysCacheCopyAttNum(RelationGetRelid(rel), attnum); + if (!HeapTupleIsValid(atttup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + attnum, RelationGetRelid(rel)); + attForm = (Form_pg_attribute) GETSTRUCT(atttup); + if (attForm->attidentity != '\0') + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("column \"%s\" of relation \"%s\" is an identity column", + get_attname(RelationGetRelid(rel), attnum, + false), + RelationGetRelationName(rel))); + + /* All good -- reset attnotnull if needed */ + if (attForm->attnotnull) + { + attForm->attnotnull = false; + CatalogTupleUpdate(attrel, &atttup->t_self, atttup); + } + + table_close(attrel, RowExclusiveLock); + } + + is_no_inherit_constraint = con->connoinherit; + + /* + * If it's a foreign-key constraint, we'd better lock the referenced table + * and check that that's not in use, just as we've already done for the + * constrained table (else we might, eg, be dropping a trigger that has + * unfired events). But we can/must skip that in the self-referential + * case. + */ + if (con->contype == CONSTRAINT_FOREIGN && + con->confrelid != RelationGetRelid(rel)) + { + Relation frel; + + /* Must match lock taken by RemoveTriggerById: */ + frel = table_open(con->confrelid, AccessExclusiveLock); + CheckAlterTableIsSafe(frel); + table_close(frel, NoLock); + } + + /* + * Perform the actual constraint deletion + */ + ObjectAddressSet(conobj, ConstraintRelationId, con->oid); + performDeletion(&conobj, behavior, 0); + + /* + * For partitioned tables, non-CHECK, non-NOT-NULL inherited constraints + * are dropped via the dependency mechanism, so we're done here. + */ + if (con->contype != CONSTRAINT_CHECK && + con->contype != CONSTRAINT_NOTNULL && + rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { + table_close(conrel, RowExclusiveLock); + return conobj; + } + + /* + * Propagate to children as appropriate. Unlike most other ALTER + * routines, we have to do this one level of recursion at a time; we can't + * use find_all_inheritors to do it in one pass. + */ + if (!is_no_inherit_constraint) + children = find_inheritance_children(RelationGetRelid(rel), lockmode); + else + children = NIL; + + foreach_oid(childrelid, children) + { + Relation childrel; + HeapTuple tuple; + Form_pg_constraint childcon; + + /* find_inheritance_children already got lock */ + childrel = table_open(childrelid, NoLock); + CheckAlterTableIsSafe(childrel); + ++======= ++>>>>>>> theirs + /* + * We search for not-null constraints by column name, and others by + * constraint name. + */ + if (con->contype == CONSTRAINT_NOTNULL) + { + tuple = findNotNullConstraint(childrelid, colname); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u", + colname, RelationGetRelid(childrel)); + } + else + { + SysScanDesc scan; + ScanKeyData skey[3]; ++<<<<<<< ours + + ScanKeyInit(&skey[0], + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(childrelid)); + ScanKeyInit(&skey[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + ScanKeyInit(&skey[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(constrName)); + scan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId, + true, NULL, 3, skey); + /* There can only be one, so no need to loop */ + tuple = systable_getnext(scan); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("constraint \"%s\" of relation \"%s\" does not exist", + constrName, + RelationGetRelationName(childrel)))); + tuple = heap_copytuple(tuple); + systable_endscan(scan); + } + + childcon = (Form_pg_constraint) GETSTRUCT(tuple); + + /* Right now only CHECK and not-null constraints can be inherited */ + if (childcon->contype != CONSTRAINT_CHECK && + childcon->contype != CONSTRAINT_NOTNULL) + elog(ERROR, "inherited constraint is not a CHECK or not-null constraint"); + + if (childcon->coninhcount <= 0) /* shouldn't happen */ + elog(ERROR, "relation %u has non-inherited constraint \"%s\"", + childrelid, NameStr(childcon->conname)); + + if (recurse) + { + /* + * If the child constraint has other definition sources, just + * decrement its inheritance count; if not, recurse to delete it. + */ + if (childcon->coninhcount == 1 && !childcon->conislocal) + { + /* Time to delete this child constraint, too */ + dropconstraint_internal(childrel, tuple, behavior, + recurse, true, missing_ok, + lockmode); + } + else + { + /* Child constraint must survive my deletion */ + childcon->coninhcount--; + CatalogTupleUpdate(conrel, &tuple->t_self, tuple); + + /* Make update visible */ + CommandCounterIncrement(); + } + } + else + { + /* + * If we were told to drop ONLY in this table (no recursion) and + * there are no further parents for this constraint, we need to + * mark the inheritors' constraints as locally defined rather than + * inherited. + */ + childcon->coninhcount--; + if (childcon->coninhcount == 0) + childcon->conislocal = true; + + CatalogTupleUpdate(conrel, &tuple->t_self, tuple); + + /* Make update visible */ + CommandCounterIncrement(); + } + + heap_freetuple(tuple); + + table_close(childrel, NoLock); + } + + table_close(conrel, RowExclusiveLock); + + return conobj; +} + +/* + * ALTER COLUMN TYPE + * + * Unlike other subcommand types, we do parse transformation for ALTER COLUMN + * TYPE during phase 1 --- the AlterTableCmd passed in here is already + * transformed (and must be, because we rely on some transformed fields). + * + * The point of this is that the execution of all ALTER COLUMN TYPEs for a + * table will be done "in parallel" during phase 3, so all the USING + * expressions should be parsed assuming the original column types. Also, + * this allows a USING expression to refer to a field that will be dropped. + * + * To make this work safely, AT_PASS_DROP then AT_PASS_ALTER_TYPE must be + * the first two execution steps in phase 2; they must not see the effects + * of any other subcommand types, since the USING expressions are parsed + * against the unmodified table's state. + */ +static void +ATPrepAlterColumnType(List **wqueue, + AlteredTableInfo *tab, Relation rel, + bool recurse, bool recursing, + AlterTableCmd *cmd, LOCKMODE lockmode, + AlterTableUtilityContext *context) +{ + char *colName = cmd->name; + ColumnDef *def = (ColumnDef *) cmd->def; + TypeName *typeName = def->typeName; + Node *transform = def->cooked_default; + HeapTuple tuple; + Form_pg_attribute attTup; + AttrNumber attnum; + Oid targettype; + int32 targettypmod; + Oid targetcollid; + NewColumnValue *newval; + ParseState *pstate = make_parsestate(NULL); + AclResult aclresult; + bool is_expr; + + pstate->p_sourcetext = context->queryString; + + if (rel->rd_rel->reloftype && !recursing) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot alter column type of typed table"), + parser_errposition(pstate, def->location))); + + /* lookup the attribute so we can check inheritance status */ + tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + colName, RelationGetRelationName(rel)), + parser_errposition(pstate, def->location))); + attTup = (Form_pg_attribute) GETSTRUCT(tuple); + attnum = attTup->attnum; + + /* Can't alter a system attribute */ + if (attnum <= 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter system column \"%s\"", colName), + parser_errposition(pstate, def->location))); + + /* + * Cannot specify USING when altering type of a generated column, because + * that would violate the generation expression. + */ + if (attTup->attgenerated && def->cooked_default) + ereport(ERROR, + (errcode(ERRCODE_INVALID_COLUMN_DEFINITION), + errmsg("cannot specify USING when altering type of generated column"), + errdetail("Column \"%s\" is a generated column.", colName), + parser_errposition(pstate, def->location))); + + /* + * Don't alter inherited columns. At outer level, there had better not be + * any inherited definition; when recursing, we assume this was checked at + * the parent level (see below). + */ + if (attTup->attinhcount > 0 && !recursing) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("cannot alter inherited column \"%s\"", colName), + parser_errposition(pstate, def->location))); + + /* Don't alter columns used in the partition key */ + if (has_partition_attrs(rel, + bms_make_singleton(attnum - FirstLowInvalidHeapAttributeNumber), + &is_expr)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("cannot alter column \"%s\" because it is part of the partition key of relation \"%s\"", + colName, RelationGetRelationName(rel)), + parser_errposition(pstate, def->location))); + + /* Look up the target type */ + typenameTypeIdAndMod(pstate, typeName, &targettype, &targettypmod); + + aclresult = object_aclcheck(TypeRelationId, targettype, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error_type(aclresult, targettype); + + /* And the collation */ + targetcollid = GetColumnDefCollation(pstate, def, targettype); + + /* make sure datatype is legal for a column */ + CheckAttributeType(colName, targettype, targetcollid, + list_make1_oid(rel->rd_rel->reltype), + (attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL ? CHKATYPE_IS_VIRTUAL : 0)); + + if (attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) + { + /* do nothing */ + } + else if (tab->relkind == RELKIND_RELATION || + tab->relkind == RELKIND_PARTITIONED_TABLE) + { + /* + * Set up an expression to transform the old data value to the new + * type. If a USING option was given, use the expression as + * transformed by transformAlterTableStmt, else just take the old + * value and try to coerce it. We do this first so that type + * incompatibility can be detected before we waste effort, and because + * we need the expression to be parsed against the original table row + * type. + */ + if (!transform) + { + transform = (Node *) makeVar(1, attnum, + attTup->atttypid, attTup->atttypmod, + attTup->attcollation, + 0); + } + + transform = coerce_to_target_type(pstate, + transform, exprType(transform), + targettype, targettypmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + -1); + if (transform == NULL) + { + /* error text depends on whether USING was specified or not */ + if (def->cooked_default != NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("result of USING clause for column \"%s\"" + " cannot be cast automatically to type %s", + colName, format_type_be(targettype)), + errhint("You might need to add an explicit cast."))); + else + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("column \"%s\" cannot be cast automatically to type %s", + colName, format_type_be(targettype)), + !attTup->attgenerated ? + /* translator: USING is SQL, don't translate it */ + errhint("You might need to specify \"USING %s::%s\".", + quote_identifier(colName), + format_type_with_typemod(targettype, + targettypmod)) : 0)); + } + + /* Fix collations after all else */ + assign_expr_collations(pstate, transform); + + /* Expand virtual generated columns in the expr. */ + transform = expand_generated_columns_in_expr(transform, rel, 1); + + /* Plan the expr now so we can accurately assess the need to rewrite. */ + transform = (Node *) expression_planner((Expr *) transform); + + /* + * Add a work queue item to make ATRewriteTable update the column + * contents. + */ + newval = palloc0_object(NewColumnValue); + newval->attnum = attnum; + newval->expr = (Expr *) transform; + newval->is_generated = false; + + tab->newvals = lappend(tab->newvals, newval); + if (ATColumnChangeRequiresRewrite(transform, attnum)) + tab->rewrite |= AT_REWRITE_COLUMN_REWRITE; + } + else if (transform) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a table", + RelationGetRelationName(rel)))); + + if (!RELKIND_HAS_STORAGE(tab->relkind) || attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) + { + /* + * For relations or columns without storage, do this check now. + * Regular tables will check it later when the table is being + * rewritten. + */ + find_composite_type_dependencies(rel->rd_rel->reltype, rel, NULL); + } + + ReleaseSysCache(tuple); + + /* + * Recurse manually by queueing a new command for each child, if + * necessary. We cannot apply ATSimpleRecursion here because we need to + * remap attribute numbers in the USING expression, if any. + * + * If we are told not to recurse, there had better not be any child + * tables; else the alter would put them out of step. + */ + if (recurse) + { + Oid relid = RelationGetRelid(rel); + List *child_oids, + *child_numparents; + ListCell *lo, + *li; + + child_oids = find_all_inheritors(relid, lockmode, + &child_numparents); + + /* + * find_all_inheritors does the recursive search of the inheritance + * hierarchy, so all we have to do is process all of the relids in the + * list that it returns. + */ + forboth(lo, child_oids, li, child_numparents) + { + Oid childrelid = lfirst_oid(lo); + int numparents = lfirst_int(li); + Relation childrel; + HeapTuple childtuple; + Form_pg_attribute childattTup; + + if (childrelid == relid) + continue; + + /* find_all_inheritors already got lock */ + childrel = relation_open(childrelid, NoLock); + CheckAlterTableIsSafe(childrel); + + /* + * Verify that the child doesn't have any inherited definitions of + * this column that came from outside this inheritance hierarchy. + * (renameatt makes a similar test, though in a different way + * because of its different recursion mechanism.) + */ + childtuple = SearchSysCacheAttName(RelationGetRelid(childrel), + colName); + if (!HeapTupleIsValid(childtuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + colName, RelationGetRelationName(childrel)))); + childattTup = (Form_pg_attribute) GETSTRUCT(childtuple); + + if (childattTup->attinhcount > numparents) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("cannot alter inherited column \"%s\" of relation \"%s\"", + colName, RelationGetRelationName(childrel)))); + + ReleaseSysCache(childtuple); + + /* + * Remap the attribute numbers. If no USING expression was + * specified, there is no need for this step. + */ + if (def->cooked_default) + { + AttrMap *attmap; + bool found_whole_row; + + /* create a copy to scribble on */ + cmd = copyObject(cmd); + + attmap = build_attrmap_by_name(RelationGetDescr(childrel), + RelationGetDescr(rel), + false); + ((ColumnDef *) cmd->def)->cooked_default = + map_variable_attnos(def->cooked_default, + 1, 0, + attmap, + InvalidOid, &found_whole_row); + if (found_whole_row) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot convert whole-row table reference"), + errdetail("USING expression contains a whole-row table reference."))); + pfree(attmap); + } + ATPrepCmd(wqueue, childrel, cmd, false, true, lockmode, context); + relation_close(childrel, NoLock); + } + } + else if (!recursing && + find_inheritance_children(RelationGetRelid(rel), NoLock) != NIL) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("type of inherited column \"%s\" must be changed in child tables too", + colName))); + + if (tab->relkind == RELKIND_COMPOSITE_TYPE) + ATTypedTableRecursion(wqueue, rel, cmd, lockmode, context); +} + +/* + * When the data type of a column is changed, a rewrite might not be required + * if the new type is sufficiently identical to the old one, and the USING + * clause isn't trying to insert some other value. It's safe to skip the + * rewrite in these cases: + * + * - the old type is binary coercible to the new type + * - the new type is an unconstrained domain over the old type + * - {NEW,OLD} or {OLD,NEW} is {timestamptz,timestamp} and the timezone is UTC + * + * In the case of a constrained domain, we could get by with scanning the + * table and checking the constraint rather than actually rewriting it, but we + * don't currently try to do that. + */ +static bool +ATColumnChangeRequiresRewrite(Node *expr, AttrNumber varattno) +{ + Assert(expr != NULL); + + for (;;) + { + /* only one varno, so no need to check that */ + if (IsA(expr, Var) && ((Var *) expr)->varattno == varattno) + return false; + else if (IsA(expr, RelabelType)) + expr = (Node *) ((RelabelType *) expr)->arg; + else if (IsA(expr, CoerceToDomain)) + { + CoerceToDomain *d = (CoerceToDomain *) expr; + + if (DomainHasConstraints(d->resulttype)) + return true; + expr = (Node *) d->arg; + } + else if (IsA(expr, FuncExpr)) + { + FuncExpr *f = (FuncExpr *) expr; + + switch (f->funcid) + { + case F_TIMESTAMPTZ_TIMESTAMP: + case F_TIMESTAMP_TIMESTAMPTZ: + if (TimestampTimestampTzRequiresRewrite()) + return true; + else + expr = linitial(f->args); + break; + default: + return true; + } + } + else + return true; + } +} + +/* + * ALTER COLUMN .. SET DATA TYPE + * + * Return the address of the modified column. + */ +static ObjectAddress +ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, + AlterTableCmd *cmd, LOCKMODE lockmode) +{ + char *colName = cmd->name; + ColumnDef *def = (ColumnDef *) cmd->def; + TypeName *typeName = def->typeName; + HeapTuple heapTup; + Form_pg_attribute attTup, + attOldTup; + AttrNumber attnum; + HeapTuple typeTuple; + Form_pg_type tform; + Oid targettype; + int32 targettypmod; + Oid targetcollid; + Node *defaultexpr; + Relation attrelation; + Relation depRel; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple depTup; + ObjectAddress address; + + /* + * Clear all the missing values if we're rewriting the table, since this + * renders them pointless. + */ + if (tab->rewrite) + { + Relation newrel; + + newrel = table_open(RelationGetRelid(rel), NoLock); + RelationClearMissing(newrel); + relation_close(newrel, NoLock); + /* make sure we don't conflict with later attribute modifications */ + CommandCounterIncrement(); + } + + attrelation = table_open(AttributeRelationId, RowExclusiveLock); + + /* Look up the target column */ + heapTup = SearchSysCacheCopyAttName(RelationGetRelid(rel), colName); + if (!HeapTupleIsValid(heapTup)) /* shouldn't happen */ + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + colName, RelationGetRelationName(rel)))); + attTup = (Form_pg_attribute) GETSTRUCT(heapTup); + attnum = attTup->attnum; + attOldTup = TupleDescAttr(tab->oldDesc, attnum - 1); + + /* Check for multiple ALTER TYPE on same column --- can't cope */ + if (attTup->atttypid != attOldTup->atttypid || + attTup->atttypmod != attOldTup->atttypmod) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type of column \"%s\" twice", + colName))); + + /* Look up the target type (should not fail, since prep found it) */ + typeTuple = typenameType(NULL, typeName, &targettypmod); + tform = (Form_pg_type) GETSTRUCT(typeTuple); + targettype = tform->oid; + /* And the collation */ + targetcollid = GetColumnDefCollation(NULL, def, targettype); + + /* + * If there is a default expression for the column, get it and ensure we + * can coerce it to the new datatype. (We must do this before changing + * the column type, because build_column_default itself will try to + * coerce, and will not issue the error message we want if it fails.) + * + * We remove any implicit coercion steps at the top level of the old + * default expression; this has been agreed to satisfy the principle of + * least surprise. (The conversion to the new column type should act like + * it started from what the user sees as the stored expression, and the + * implicit coercions aren't going to be shown.) + */ + if (attTup->atthasdef) + { + defaultexpr = build_column_default(rel, attnum); + Assert(defaultexpr); + defaultexpr = strip_implicit_coercions(defaultexpr); + defaultexpr = coerce_to_target_type(NULL, /* no UNKNOWN params */ + defaultexpr, exprType(defaultexpr), + targettype, targettypmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + -1); + if (defaultexpr == NULL) + { + if (attTup->attgenerated) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("generation expression for column \"%s\" cannot be cast automatically to type %s", + colName, format_type_be(targettype)))); + else + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("default for column \"%s\" cannot be cast automatically to type %s", + colName, format_type_be(targettype)))); + } + } + else + defaultexpr = NULL; + + /* + * Find everything that depends on the column (constraints, indexes, etc), + * and record enough information to let us recreate the objects. + * + * The actual recreation does not happen here, but only after we have + * performed all the individual ALTER TYPE operations. We have to save + * the info before executing ALTER TYPE, though, else the deparser will + * get confused. + */ + RememberAllDependentForRebuilding(tab, AT_AlterColumnType, rel, attnum, colName); + + /* + * Now scan for dependencies of this column on other things. The only + * things we should find are the dependency on the column datatype and + * possibly a collation dependency. Those can be removed. + */ + depRel = table_open(DependRelationId, RowExclusiveLock); + + ScanKeyInit(&key[0], + Anum_pg_depend_classid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationRelationId)); + ScanKeyInit(&key[1], + Anum_pg_depend_objid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(rel))); + ScanKeyInit(&key[2], + Anum_pg_depend_objsubid, + BTEqualStrategyNumber, F_INT4EQ, + Int32GetDatum((int32) attnum)); + + scan = systable_beginscan(depRel, DependDependerIndexId, true, + NULL, 3, key); + + while (HeapTupleIsValid(depTup = systable_getnext(scan))) + { + Form_pg_depend foundDep = (Form_pg_depend) GETSTRUCT(depTup); + ObjectAddress foundObject; + + foundObject.classId = foundDep->refclassid; + foundObject.objectId = foundDep->refobjid; + foundObject.objectSubId = foundDep->refobjsubid; + + if (foundDep->deptype != DEPENDENCY_NORMAL) + elog(ERROR, "found unexpected dependency type '%c'", + foundDep->deptype); + if (!(foundDep->refclassid == TypeRelationId && + foundDep->refobjid == attTup->atttypid) && + !(foundDep->refclassid == CollationRelationId && + foundDep->refobjid == attTup->attcollation)) + elog(ERROR, "found unexpected dependency for column: %s", + getObjectDescription(&foundObject, false)); + + CatalogTupleDelete(depRel, &depTup->t_self); + } + + systable_endscan(scan); + + table_close(depRel, RowExclusiveLock); + + /* + * Here we go --- change the recorded column type and collation. (Note + * heapTup is a copy of the syscache entry, so okay to scribble on.) First + * fix up the missing value if any. + */ + if (attTup->atthasmissing) + { + Datum missingval; + bool missingNull; + + /* if rewrite is true the missing value should already be cleared */ + Assert(tab->rewrite == 0); + + /* Get the missing value datum */ + missingval = heap_getattr(heapTup, + Anum_pg_attribute_attmissingval, + attrelation->rd_att, + &missingNull); + + /* if it's a null array there is nothing to do */ + + if (!missingNull) + { + /* + * Get the datum out of the array and repack it in a new array + * built with the new type data. We assume that since the table + * doesn't need rewriting, the actual Datum doesn't need to be + * changed, only the array metadata. + */ + + int one = 1; + bool isNull; + Datum valuesAtt[Natts_pg_attribute] = {0}; + bool nullsAtt[Natts_pg_attribute] = {0}; + bool replacesAtt[Natts_pg_attribute] = {0}; + HeapTuple newTup; + + missingval = array_get_element(missingval, + 1, + &one, + 0, + attTup->attlen, + attTup->attbyval, + attTup->attalign, + &isNull); + missingval = PointerGetDatum(construct_array(&missingval, + 1, + targettype, + tform->typlen, + tform->typbyval, + tform->typalign)); + + valuesAtt[Anum_pg_attribute_attmissingval - 1] = missingval; + replacesAtt[Anum_pg_attribute_attmissingval - 1] = true; + nullsAtt[Anum_pg_attribute_attmissingval - 1] = false; + + newTup = heap_modify_tuple(heapTup, RelationGetDescr(attrelation), + valuesAtt, nullsAtt, replacesAtt); + heap_freetuple(heapTup); + heapTup = newTup; + attTup = (Form_pg_attribute) GETSTRUCT(heapTup); + } + } + + attTup->atttypid = targettype; + attTup->atttypmod = targettypmod; + attTup->attcollation = targetcollid; + if (list_length(typeName->arrayBounds) > PG_INT16_MAX) + ereport(ERROR, + errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("too many array dimensions")); + attTup->attndims = list_length(typeName->arrayBounds); + attTup->attlen = tform->typlen; + attTup->attbyval = tform->typbyval; + attTup->attalign = tform->typalign; + attTup->attstorage = tform->typstorage; + attTup->attcompression = InvalidCompressionMethod; + + ReleaseSysCache(typeTuple); + + CatalogTupleUpdate(attrelation, &heapTup->t_self, heapTup); + + table_close(attrelation, RowExclusiveLock); + + /* Install dependencies on new datatype and collation */ + add_column_datatype_dependency(RelationGetRelid(rel), attnum, targettype); + add_column_collation_dependency(RelationGetRelid(rel), attnum, targetcollid); + + /* + * Drop any pg_statistic entry for the column, since it's now wrong type + */ + RemoveStatistics(RelationGetRelid(rel), attnum); + + InvokeObjectPostAlterHook(RelationRelationId, + RelationGetRelid(rel), attnum); + + /* + * Update the default, if present, by brute force --- remove and re-add + * the default. Probably unsafe to take shortcuts, since the new version + * may well have additional dependencies. (It's okay to do this now, + * rather than after other ALTER TYPE commands, since the default won't + * depend on other column types.) + */ + if (defaultexpr) + { + /* + * If it's a GENERATED default, drop its dependency records, in + * particular its INTERNAL dependency on the column, which would + * otherwise cause dependency.c to refuse to perform the deletion. + */ + if (attTup->attgenerated) + { + Oid attrdefoid = GetAttrDefaultOid(RelationGetRelid(rel), attnum); + + if (!OidIsValid(attrdefoid)) + elog(ERROR, "could not find attrdef tuple for relation %u attnum %d", + RelationGetRelid(rel), attnum); + (void) deleteDependencyRecordsFor(AttrDefaultRelationId, attrdefoid, false); + } + + /* + * Make updates-so-far visible, particularly the new pg_attribute row + * which will be updated again. + */ + CommandCounterIncrement(); + + /* + * We use RESTRICT here for safety, but at present we do not expect + * anything to depend on the default. + */ + RemoveAttrDefault(RelationGetRelid(rel), attnum, DROP_RESTRICT, true, + true); + + (void) StoreAttrDefault(rel, attnum, defaultexpr, true); + } + + ObjectAddressSubSet(address, RelationRelationId, + RelationGetRelid(rel), attnum); + + /* Cleanup */ + heap_freetuple(heapTup); + + return address; +} + +/* + * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything + * that depends on the column (constraints, indexes, etc), and record enough + * information to let us recreate the objects. + */ +static void +RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, + Relation rel, AttrNumber attnum, const char *colName) +{ + Relation depRel; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple depTup; + + Assert(subtype == AT_AlterColumnType || subtype == AT_SetExpression); + + depRel = table_open(DependRelationId, RowExclusiveLock); + + ScanKeyInit(&key[0], + Anum_pg_depend_refclassid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationRelationId)); + ScanKeyInit(&key[1], + Anum_pg_depend_refobjid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(rel))); + ScanKeyInit(&key[2], + Anum_pg_depend_refobjsubid, + BTEqualStrategyNumber, F_INT4EQ, + Int32GetDatum((int32) attnum)); + + scan = systable_beginscan(depRel, DependReferenceIndexId, true, + NULL, 3, key); + + while (HeapTupleIsValid(depTup = systable_getnext(scan))) + { + Form_pg_depend foundDep = (Form_pg_depend) GETSTRUCT(depTup); + ObjectAddress foundObject; + + foundObject.classId = foundDep->classid; + foundObject.objectId = foundDep->objid; + foundObject.objectSubId = foundDep->objsubid; + + switch (foundObject.classId) + { + case RelationRelationId: + { + char relKind = get_rel_relkind(foundObject.objectId); + + if (relKind == RELKIND_INDEX || + relKind == RELKIND_PARTITIONED_INDEX) + { + Assert(foundObject.objectSubId == 0); + RememberIndexForRebuilding(foundObject.objectId, tab); + } + else if (relKind == RELKIND_SEQUENCE) + { + /* + * This must be a SERIAL column's sequence. We need + * not do anything to it. + */ + Assert(foundObject.objectSubId == 0); + } + else + { + /* Not expecting any other direct dependencies... */ + elog(ERROR, "unexpected object depending on column: %s", + getObjectDescription(&foundObject, false)); + } + break; + } + + case ConstraintRelationId: + Assert(foundObject.objectSubId == 0); + RememberConstraintForRebuilding(foundObject.objectId, tab); + break; + + case ProcedureRelationId: + + /* + * A new-style SQL function can depend on a column, if that + * column is referenced in the parsed function body. Ideally + * we'd automatically update the function by deparsing and + * reparsing it, but that's risky and might well fail anyhow. + * FIXME someday. + * + * This is only a problem for AT_AlterColumnType, not + * AT_SetExpression. + */ + if (subtype == AT_AlterColumnType) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type of a column used by a function or procedure"), + errdetail("%s depends on column \"%s\"", + getObjectDescription(&foundObject, false), + colName))); + break; + + case RewriteRelationId: + + /* + * View/rule bodies have pretty much the same issues as + * function bodies. FIXME someday. + */ + if (subtype == AT_AlterColumnType) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type of a column used by a view or rule"), + errdetail("%s depends on column \"%s\"", + getObjectDescription(&foundObject, false), + colName))); + break; + + case TriggerRelationId: + + /* + * A trigger can depend on a column because the column is + * specified as an update target, or because the column is + * used in the trigger's WHEN condition. The first case would + * not require any extra work, but the second case would + * require updating the WHEN expression, which has the same + * issues as above. Since we can't easily tell which case + * applies, we punt for both. FIXME someday. + */ + if (subtype == AT_AlterColumnType) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type of a column used in a trigger definition"), + errdetail("%s depends on column \"%s\"", + getObjectDescription(&foundObject, false), + colName))); + break; + + case PolicyRelationId: + + /* + * A policy can depend on a column because the column is + * specified in the policy's USING or WITH CHECK qual + * expressions. It might be possible to rewrite and recheck + * the policy expression, but punt for now. It's certainly + * easy enough to remove and recreate the policy; still, FIXME + * someday. + */ + if (subtype == AT_AlterColumnType) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type of a column used in a policy definition"), + errdetail("%s depends on column \"%s\"", + getObjectDescription(&foundObject, false), + colName))); + break; + + case AttrDefaultRelationId: + { + ObjectAddress col = GetAttrDefaultColumnAddress(foundObject.objectId); + + if (col.objectId == RelationGetRelid(rel) && + col.objectSubId == attnum) + { + /* + * Ignore the column's own default expression. The + * caller deals with it. + */ + } + else + { + /* + * This must be a reference from the expression of a + * generated column elsewhere in the same table. + * Changing the type/generated expression of a column + * that is used by a generated column is not allowed + * by SQL standard, so just punt for now. It might be + * doable with some thinking and effort. + */ + if (subtype == AT_AlterColumnType) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type of a column used by a generated column"), + errdetail("Column \"%s\" is used by generated column \"%s\".", + colName, + get_attname(col.objectId, + col.objectSubId, + false)))); + } + break; + } + + case StatisticExtRelationId: + + /* + * Give the extended-stats machinery a chance to fix anything + * that this column type change would break. + */ + RememberStatisticsForRebuilding(foundObject.objectId, tab); + break; + + case PublicationRelRelationId: + + /* + * Column reference in a PUBLICATION ... FOR TABLE ... WHERE + * clause. Same issues as above. FIXME someday. + */ + if (subtype == AT_AlterColumnType) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type of a column used by a publication WHERE clause"), + errdetail("%s depends on column \"%s\"", + getObjectDescription(&foundObject, false), + colName))); + break; + + default: + + /* + * We don't expect any other sorts of objects to depend on a + * column. + */ + elog(ERROR, "unexpected object depending on column: %s", + getObjectDescription(&foundObject, false)); + break; + } + } + + systable_endscan(scan); + table_close(depRel, NoLock); +} + +/* + * Subroutine for ATExecAlterColumnType: remember that a replica identity + * needs to be reset. + */ +static void +RememberReplicaIdentityForRebuilding(Oid indoid, AlteredTableInfo *tab) +{ + if (!get_index_isreplident(indoid)) + return; + + if (tab->replicaIdentityIndex) + elog(ERROR, "relation %u has multiple indexes marked as replica identity", tab->relid); + + tab->replicaIdentityIndex = get_rel_name(indoid); +} + +/* + * Subroutine for ATExecAlterColumnType: remember any clustered index. + */ +static void +RememberClusterOnForRebuilding(Oid indoid, AlteredTableInfo *tab) +{ + if (!get_index_isclustered(indoid)) + return; + + if (tab->clusterOnIndex) + elog(ERROR, "relation %u has multiple clustered indexes", tab->relid); + + tab->clusterOnIndex = get_rel_name(indoid); +} + +/* + * Subroutine for ATExecAlterColumnType: remember that a constraint needs + * to be rebuilt (which we might already know). + */ +static void +RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab) +{ + /* + * This de-duplication check is critical for two independent reasons: we + * mustn't try to recreate the same constraint twice, and if a constraint + * depends on more than one column whose type is to be altered, we must + * capture its definition string before applying any of the column type + * changes. ruleutils.c will get confused if we ask again later. + */ + if (!list_member_oid(tab->changedConstraintOids, conoid)) + { + /* OK, capture the constraint's existing definition string */ + char *defstring = pg_get_constraintdef_command(conoid); + Oid indoid; + + /* + * It is critical to create not-null constraints ahead of primary key + * indexes; otherwise, the not-null constraint would be created by the + * primary key, and the constraint name would be wrong. + */ + if (get_constraint_type(conoid) == CONSTRAINT_NOTNULL) + { + tab->changedConstraintOids = lcons_oid(conoid, + tab->changedConstraintOids); + tab->changedConstraintDefs = lcons(defstring, + tab->changedConstraintDefs); + } + else + { + + tab->changedConstraintOids = lappend_oid(tab->changedConstraintOids, + conoid); + tab->changedConstraintDefs = lappend(tab->changedConstraintDefs, + defstring); + } + + /* + * For the index of a constraint, if any, remember if it is used for + * the table's replica identity or if it is a clustered index, so that + * ATPostAlterTypeCleanup() can queue up commands necessary to restore + * those properties. + */ + indoid = get_constraint_index(conoid); + if (OidIsValid(indoid)) + { + RememberReplicaIdentityForRebuilding(indoid, tab); + RememberClusterOnForRebuilding(indoid, tab); + } + } +} + +/* + * Subroutine for ATExecAlterColumnType: remember that an index needs + * to be rebuilt (which we might already know). + */ +static void +RememberIndexForRebuilding(Oid indoid, AlteredTableInfo *tab) +{ + /* + * This de-duplication check is critical for two independent reasons: we + * mustn't try to recreate the same index twice, and if an index depends + * on more than one column whose type is to be altered, we must capture + * its definition string before applying any of the column type changes. + * ruleutils.c will get confused if we ask again later. + */ + if (!list_member_oid(tab->changedIndexOids, indoid)) + { + /* + * Before adding it as an index-to-rebuild, we'd better see if it + * belongs to a constraint, and if so rebuild the constraint instead. + * Typically this check fails, because constraint indexes normally + * have only dependencies on their constraint. But it's possible for + * such an index to also have direct dependencies on table columns, + * for example with a partial exclusion constraint. + */ + Oid conoid = get_index_constraint(indoid); + + if (OidIsValid(conoid)) + { + RememberConstraintForRebuilding(conoid, tab); + } + else + { + /* OK, capture the index's existing definition string */ + char *defstring = pg_get_indexdef_string(indoid); + + tab->changedIndexOids = lappend_oid(tab->changedIndexOids, + indoid); + tab->changedIndexDefs = lappend(tab->changedIndexDefs, + defstring); + + /* + * Remember if this index is used for the table's replica identity + * or if it is a clustered index, so that ATPostAlterTypeCleanup() + * can queue up commands necessary to restore those properties. + */ + RememberReplicaIdentityForRebuilding(indoid, tab); + RememberClusterOnForRebuilding(indoid, tab); + } + } +} + +/* + * Subroutine for ATExecAlterColumnType: remember that a statistics object + * needs to be rebuilt (which we might already know). + */ +static void +RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab) +{ + /* + * This de-duplication check is critical for two independent reasons: we + * mustn't try to recreate the same statistics object twice, and if the + * statistics object depends on more than one column whose type is to be + * altered, we must capture its definition string before applying any of + * the type changes. ruleutils.c will get confused if we ask again later. + */ + if (!list_member_oid(tab->changedStatisticsOids, stxoid)) + { + /* OK, capture the statistics object's existing definition string */ + char *defstring = pg_get_statisticsobjdef_string(stxoid); + + tab->changedStatisticsOids = lappend_oid(tab->changedStatisticsOids, + stxoid); + tab->changedStatisticsDefs = lappend(tab->changedStatisticsDefs, + defstring); + } +} + +/* + * Cleanup after we've finished all the ALTER TYPE or SET EXPRESSION + * operations for a particular relation. We have to drop and recreate all the + * indexes and constraints that depend on the altered columns. We do the + * actual dropping here, but re-creation is managed by adding work queue + * entries to do those steps later. + */ +static void +ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) +{ + ObjectAddress obj; + ObjectAddresses *objects; + ListCell *def_item; + ListCell *oid_item; + + /* + * Collect all the constraints and indexes to drop so we can process them + * in a single call. That way we don't have to worry about dependencies + * among them. + */ + objects = new_object_addresses(); + + /* + * Re-parse the index and constraint definitions, and attach them to the + * appropriate work queue entries. We do this before dropping because in + * the case of a constraint on another table, we might not yet have + * exclusive lock on the table the constraint is attached to, and we need + * to get that before reparsing/dropping. (That's possible at least for + * FOREIGN KEY, CHECK, and EXCLUSION constraints; in non-FK cases it + * requires a dependency on the target table's composite type in the other + * table's constraint expressions.) + * + * We can't rely on the output of deparsing to tell us which relation to + * operate on, because concurrent activity might have made the name + * resolve differently. Instead, we've got to use the OID of the + * constraint or index we're processing to figure out which relation to + * operate on. + */ + forboth(oid_item, tab->changedConstraintOids, + def_item, tab->changedConstraintDefs) + { + Oid oldId = lfirst_oid(oid_item); + HeapTuple tup; + Form_pg_constraint con; + Oid relid; + Oid confrelid; + bool conislocal; + + tup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(oldId)); + if (!HeapTupleIsValid(tup)) /* should not happen */ + elog(ERROR, "cache lookup failed for constraint %u", oldId); + con = (Form_pg_constraint) GETSTRUCT(tup); + if (OidIsValid(con->conrelid)) + relid = con->conrelid; + else + { + /* must be a domain constraint */ + relid = get_typ_typrelid(getBaseType(con->contypid)); + if (!OidIsValid(relid)) + elog(ERROR, "could not identify relation associated with constraint %u", oldId); + } + confrelid = con->confrelid; + conislocal = con->conislocal; + ReleaseSysCache(tup); + + ObjectAddressSet(obj, ConstraintRelationId, oldId); + add_exact_object_address(&obj, objects); + + /* + * If the constraint is inherited (only), we don't want to inject a + * new definition here; it'll get recreated when + * ATAddCheckNNConstraint recurses from adding the parent table's + * constraint. But we had to carry the info this far so that we can + * drop the constraint below. + */ + if (!conislocal) + continue; + + /* + * When rebuilding another table's constraint that references the + * table we're modifying, we might not yet have any lock on the other + * table, so get one now. We'll need AccessExclusiveLock for the DROP + * CONSTRAINT step, so there's no value in asking for anything weaker. + */ + if (relid != tab->relid) + LockRelationOid(relid, AccessExclusiveLock); + + ATPostAlterTypeParse(oldId, relid, confrelid, + (char *) lfirst(def_item), + wqueue, lockmode, tab->rewrite); + } + forboth(oid_item, tab->changedIndexOids, + def_item, tab->changedIndexDefs) + { + Oid oldId = lfirst_oid(oid_item); + Oid relid; + + relid = IndexGetRelation(oldId, false); + + /* + * As above, make sure we have lock on the index's table if it's not + * the same table. + */ + if (relid != tab->relid) + LockRelationOid(relid, AccessExclusiveLock); + + ATPostAlterTypeParse(oldId, relid, InvalidOid, + (char *) lfirst(def_item), + wqueue, lockmode, tab->rewrite); + + ObjectAddressSet(obj, RelationRelationId, oldId); + add_exact_object_address(&obj, objects); + } + + /* add dependencies for new statistics */ + forboth(oid_item, tab->changedStatisticsOids, + def_item, tab->changedStatisticsDefs) + { + Oid oldId = lfirst_oid(oid_item); + Oid relid; + + relid = StatisticsGetRelation(oldId, false); + + /* + * As above, make sure we have lock on the statistics object's table + * if it's not the same table. However, we take + * ShareUpdateExclusiveLock here, aligning with the lock level used in + * CreateStatistics and RemoveStatisticsById. + * + * CAUTION: this should be done after all cases that grab + * AccessExclusiveLock, else we risk causing deadlock due to needing + * to promote our table lock. + */ + if (relid != tab->relid) + LockRelationOid(relid, ShareUpdateExclusiveLock); + + ATPostAlterTypeParse(oldId, relid, InvalidOid, + (char *) lfirst(def_item), + wqueue, lockmode, tab->rewrite); + + ObjectAddressSet(obj, StatisticExtRelationId, oldId); + add_exact_object_address(&obj, objects); + } + + /* + * Queue up command to restore replica identity index marking + */ + if (tab->replicaIdentityIndex) + { + AlterTableCmd *cmd = makeNode(AlterTableCmd); + ReplicaIdentityStmt *subcmd = makeNode(ReplicaIdentityStmt); + + subcmd->identity_type = REPLICA_IDENTITY_INDEX; + subcmd->name = tab->replicaIdentityIndex; + cmd->subtype = AT_ReplicaIdentity; + cmd->def = (Node *) subcmd; + + /* do it after indexes and constraints */ + tab->subcmds[AT_PASS_OLD_CONSTR] = + lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd); + } + + /* + * Queue up command to restore marking of index used for cluster. + */ + if (tab->clusterOnIndex) + { + AlterTableCmd *cmd = makeNode(AlterTableCmd); + + cmd->subtype = AT_ClusterOn; + cmd->name = tab->clusterOnIndex; + + /* do it after indexes and constraints */ + tab->subcmds[AT_PASS_OLD_CONSTR] = + lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd); + } + + /* + * It should be okay to use DROP_RESTRICT here, since nothing else should + * be depending on these objects. + */ + performMultipleDeletions(objects, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); + + free_object_addresses(objects); + + /* + * The objects will get recreated during subsequent passes over the work + * queue. + */ +} + +/* + * Parse the previously-saved definition string for a constraint, index or + * statistics object against the newly-established column data type(s), and + * queue up the resulting command parsetrees for execution. + * + * This might fail if, for example, you have a WHERE clause that uses an + * operator that's not available for the new column type. + */ +static void +ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd, + List **wqueue, LOCKMODE lockmode, bool rewrite) +{ + List *raw_parsetree_list; + List *querytree_list; + ListCell *list_item; + Relation rel; + + /* + * We expect that we will get only ALTER TABLE and CREATE INDEX + * statements. Hence, there is no need to pass them through + * parse_analyze_*() or the rewriter, but instead we need to pass them + * through parse_utilcmd.c to make them ready for execution. + */ + raw_parsetree_list = raw_parser(cmd, RAW_PARSE_DEFAULT); + querytree_list = NIL; + foreach(list_item, raw_parsetree_list) + { + RawStmt *rs = lfirst_node(RawStmt, list_item); + Node *stmt = rs->stmt; + + if (IsA(stmt, IndexStmt)) + querytree_list = lappend(querytree_list, + transformIndexStmt(oldRelId, + (IndexStmt *) stmt, + cmd)); + else if (IsA(stmt, AlterTableStmt)) + { + List *beforeStmts; + List *afterStmts; + + stmt = (Node *) transformAlterTableStmt(oldRelId, + (AlterTableStmt *) stmt, + cmd, + &beforeStmts, + &afterStmts); + querytree_list = list_concat(querytree_list, beforeStmts); + querytree_list = lappend(querytree_list, stmt); + querytree_list = list_concat(querytree_list, afterStmts); + } + else if (IsA(stmt, CreateStatsStmt)) + querytree_list = lappend(querytree_list, + transformStatsStmt(oldRelId, + (CreateStatsStmt *) stmt, + cmd)); + else + querytree_list = lappend(querytree_list, stmt); + } + + /* Caller should already have acquired whatever lock we need. */ + rel = relation_open(oldRelId, NoLock); + + /* + * Attach each generated command to the proper place in the work queue. + * Note this could result in creation of entirely new work-queue entries. + * + * Also note that we have to tweak the command subtypes, because it turns + * out that re-creation of indexes and constraints has to act a bit + * differently from initial creation. + */ + foreach(list_item, querytree_list) + { + Node *stm = (Node *) lfirst(list_item); + AlteredTableInfo *tab; + + tab = ATGetQueueEntry(wqueue, rel); + + if (IsA(stm, IndexStmt)) + { + IndexStmt *stmt = (IndexStmt *) stm; + AlterTableCmd *newcmd; + + if (!rewrite) + TryReuseIndex(oldId, stmt); + stmt->reset_default_tblspc = true; + /* keep the index's comment */ + stmt->idxcomment = GetComment(oldId, RelationRelationId, 0); + + newcmd = makeNode(AlterTableCmd); + newcmd->subtype = AT_ReAddIndex; + newcmd->def = (Node *) stmt; + tab->subcmds[AT_PASS_OLD_INDEX] = + lappend(tab->subcmds[AT_PASS_OLD_INDEX], newcmd); + } + else if (IsA(stm, AlterTableStmt)) + { + AlterTableStmt *stmt = (AlterTableStmt *) stm; + ListCell *lcmd; + + foreach(lcmd, stmt->cmds) + { + AlterTableCmd *cmd = lfirst_node(AlterTableCmd, lcmd); + + if (cmd->subtype == AT_AddIndex) + { + IndexStmt *indstmt; + Oid indoid; + + indstmt = castNode(IndexStmt, cmd->def); + indoid = get_constraint_index(oldId); + + if (!rewrite) + TryReuseIndex(indoid, indstmt); + /* keep any comment on the index */ + indstmt->idxcomment = GetComment(indoid, + RelationRelationId, 0); + indstmt->reset_default_tblspc = true; + + cmd->subtype = AT_ReAddIndex; + tab->subcmds[AT_PASS_OLD_INDEX] = + lappend(tab->subcmds[AT_PASS_OLD_INDEX], cmd); + + /* recreate any comment on the constraint */ + RebuildConstraintComment(tab, + AT_PASS_OLD_INDEX, + oldId, + rel, + NIL, + indstmt->idxname); + } + else if (cmd->subtype == AT_AddConstraint) + { + Constraint *con = castNode(Constraint, cmd->def); + + con->old_pktable_oid = refRelId; + /* rewriting neither side of a FK */ + if (con->contype == CONSTR_FOREIGN && + !rewrite && tab->rewrite == 0) + TryReuseForeignKey(oldId, con); + con->reset_default_tblspc = true; + cmd->subtype = AT_ReAddConstraint; + tab->subcmds[AT_PASS_OLD_CONSTR] = + lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd); + + /* + * Recreate any comment on the constraint. If we have + * recreated a primary key, then transformTableConstraint + * has added an unnamed not-null constraint here; skip + * this in that case. + */ + if (con->conname) + RebuildConstraintComment(tab, + AT_PASS_OLD_CONSTR, + oldId, + rel, + NIL, + con->conname); + else + Assert(con->contype == CONSTR_NOTNULL); + } + else + elog(ERROR, "unexpected statement subtype: %d", + (int) cmd->subtype); + } + } + else if (IsA(stm, AlterDomainStmt)) + { + AlterDomainStmt *stmt = (AlterDomainStmt *) stm; + + if (stmt->subtype == AD_AddConstraint) + { + Constraint *con = castNode(Constraint, stmt->def); + AlterTableCmd *cmd = makeNode(AlterTableCmd); + + cmd->subtype = AT_ReAddDomainConstraint; + cmd->def = (Node *) stmt; + tab->subcmds[AT_PASS_OLD_CONSTR] = + lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd); + + /* recreate any comment on the constraint */ + RebuildConstraintComment(tab, + AT_PASS_OLD_CONSTR, + oldId, + NULL, + stmt->typeName, + con->conname); + } + else + elog(ERROR, "unexpected statement subtype: %d", + (int) stmt->subtype); + } + else if (IsA(stm, CreateStatsStmt)) + { + CreateStatsStmt *stmt = (CreateStatsStmt *) stm; + AlterTableCmd *newcmd; + + /* keep the statistics object's comment */ + stmt->stxcomment = GetComment(oldId, StatisticExtRelationId, 0); + + newcmd = makeNode(AlterTableCmd); + newcmd->subtype = AT_ReAddStatistics; + newcmd->def = (Node *) stmt; + tab->subcmds[AT_PASS_MISC] = + lappend(tab->subcmds[AT_PASS_MISC], newcmd); + } + else + elog(ERROR, "unexpected statement type: %d", + (int) nodeTag(stm)); + } + + relation_close(rel, NoLock); +} + +/* + * Subroutine for ATPostAlterTypeParse() to recreate any existing comment + * for a table or domain constraint that is being rebuilt. + * + * objid is the OID of the constraint. + * Pass "rel" for a table constraint, or "domname" (domain's qualified name + * as a string list) for a domain constraint. + * (We could dig that info, as well as the conname, out of the pg_constraint + * entry; but callers already have them so might as well pass them.) + */ +static void +RebuildConstraintComment(AlteredTableInfo *tab, AlterTablePass pass, Oid objid, + Relation rel, List *domname, + const char *conname) +{ + CommentStmt *cmd; + char *comment_str; + AlterTableCmd *newcmd; + + /* Look for comment for object wanted, and leave if none */ + comment_str = GetComment(objid, ConstraintRelationId, 0); + if (comment_str == NULL) + return; + + /* Build CommentStmt node, copying all input data for safety */ + cmd = makeNode(CommentStmt); + if (rel) + { + cmd->objtype = OBJECT_TABCONSTRAINT; + cmd->object = (Node *) + list_make3(makeString(get_namespace_name(RelationGetNamespace(rel))), + makeString(pstrdup(RelationGetRelationName(rel))), + makeString(pstrdup(conname))); + } + else + { + cmd->objtype = OBJECT_DOMCONSTRAINT; + cmd->object = (Node *) + list_make2(makeTypeNameFromNameList(copyObject(domname)), + makeString(pstrdup(conname))); + } + cmd->comment = comment_str; + + /* Append it to list of commands */ + newcmd = makeNode(AlterTableCmd); + newcmd->subtype = AT_ReAddComment; + newcmd->def = (Node *) cmd; + tab->subcmds[pass] = lappend(tab->subcmds[pass], newcmd); +} + +/* + * Subroutine for ATPostAlterTypeParse(). Calls out to CheckIndexCompatible() + * for the real analysis, then mutates the IndexStmt based on that verdict. + */ +static void +TryReuseIndex(Oid oldId, IndexStmt *stmt) +{ + if (CheckIndexCompatible(oldId, + stmt->accessMethod, + stmt->indexParams, + stmt->excludeOpNames, + stmt->iswithoutoverlaps)) + { + Relation irel = index_open(oldId, NoLock); + + /* If it's a partitioned index, there is no storage to share. */ + if (irel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX) + { + stmt->oldNumber = irel->rd_locator.relNumber; + stmt->oldCreateSubid = irel->rd_createSubid; + stmt->oldFirstRelfilelocatorSubid = irel->rd_firstRelfilelocatorSubid; + } + index_close(irel, NoLock); + } +} + +/* + * Subroutine for ATPostAlterTypeParse(). + * + * Stash the old P-F equality operator into the Constraint node, for possible + * use by ATAddForeignKeyConstraint() in determining whether revalidation of + * this constraint can be skipped. + */ +static void +TryReuseForeignKey(Oid oldId, Constraint *con) +{ + HeapTuple tup; + Datum adatum; + ArrayType *arr; + Oid *rawarr; + int numkeys; + int i; + + Assert(con->contype == CONSTR_FOREIGN); + Assert(con->old_conpfeqop == NIL); /* already prepared this node */ + + tup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(oldId)); + if (!HeapTupleIsValid(tup)) /* should not happen */ + elog(ERROR, "cache lookup failed for constraint %u", oldId); + + adatum = SysCacheGetAttrNotNull(CONSTROID, tup, + Anum_pg_constraint_conpfeqop); + arr = DatumGetArrayTypeP(adatum); /* ensure not toasted */ + numkeys = ARR_DIMS(arr)[0]; + /* test follows the one in ri_FetchConstraintInfo() */ + if (ARR_NDIM(arr) != 1 || + ARR_HASNULL(arr) || + ARR_ELEMTYPE(arr) != OIDOID) + elog(ERROR, "conpfeqop is not a 1-D Oid array"); + rawarr = (Oid *) ARR_DATA_PTR(arr); + + /* stash a List of the operator Oids in our Constraint node */ + for (i = 0; i < numkeys; i++) + con->old_conpfeqop = lappend_oid(con->old_conpfeqop, rawarr[i]); + + ReleaseSysCache(tup); +} + +/* + * ALTER COLUMN .. OPTIONS ( ... ) + * + * Returns the address of the modified column + */ +static ObjectAddress +ATExecAlterColumnGenericOptions(Relation rel, + const char *colName, + List *options, + LOCKMODE lockmode) +{ + Relation ftrel; + Relation attrel; + ForeignServer *server; + ForeignDataWrapper *fdw; + HeapTuple tuple; + HeapTuple newtuple; + bool isnull; + Datum repl_val[Natts_pg_attribute]; + bool repl_null[Natts_pg_attribute]; + bool repl_repl[Natts_pg_attribute]; + Datum datum; + Form_pg_foreign_table fttableform; + Form_pg_attribute atttableform; + AttrNumber attnum; + ObjectAddress address; + + if (options == NIL) + return InvalidObjectAddress; + + /* First, determine FDW validator associated to the foreign table. */ + ftrel = table_open(ForeignTableRelationId, AccessShareLock); + tuple = SearchSysCache1(FOREIGNTABLEREL, ObjectIdGetDatum(rel->rd_id)); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("foreign table \"%s\" does not exist", + RelationGetRelationName(rel)))); + fttableform = (Form_pg_foreign_table) GETSTRUCT(tuple); + server = GetForeignServer(fttableform->ftserver); + fdw = GetForeignDataWrapper(server->fdwid); + + table_close(ftrel, AccessShareLock); + ReleaseSysCache(tuple); + + attrel = table_open(AttributeRelationId, RowExclusiveLock); + tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + colName, RelationGetRelationName(rel)))); + + /* Prevent them from altering a system attribute */ + atttableform = (Form_pg_attribute) GETSTRUCT(tuple); + attnum = atttableform->attnum; + if (attnum <= 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter system column \"%s\"", colName))); + + + /* Initialize buffers for new tuple values */ + memset(repl_val, 0, sizeof(repl_val)); + memset(repl_null, false, sizeof(repl_null)); + memset(repl_repl, false, sizeof(repl_repl)); + + /* Extract the current options */ + datum = SysCacheGetAttr(ATTNAME, + tuple, + Anum_pg_attribute_attfdwoptions, + &isnull); + if (isnull) + datum = PointerGetDatum(NULL); + + /* Transform the options */ + datum = transformGenericOptions(AttributeRelationId, + datum, + options, + fdw->fdwvalidator); + + if (DatumGetPointer(datum) != NULL) + repl_val[Anum_pg_attribute_attfdwoptions - 1] = datum; + else + repl_null[Anum_pg_attribute_attfdwoptions - 1] = true; + + repl_repl[Anum_pg_attribute_attfdwoptions - 1] = true; + + /* Everything looks good - update the tuple */ + + newtuple = heap_modify_tuple(tuple, RelationGetDescr(attrel), + repl_val, repl_null, repl_repl); + + CatalogTupleUpdate(attrel, &newtuple->t_self, newtuple); + + InvokeObjectPostAlterHook(RelationRelationId, + RelationGetRelid(rel), + atttableform->attnum); + ObjectAddressSubSet(address, RelationRelationId, + RelationGetRelid(rel), attnum); + + ReleaseSysCache(tuple); + + table_close(attrel, RowExclusiveLock); + + heap_freetuple(newtuple); + + return address; +} ++======= ++>>>>>>> theirs + - /* - * ALTER TABLE OWNER - * - * recursing is true if we are recursing from a table to its indexes, - * sequences, or toast table. We don't allow the ownership of those things to - * be changed separately from the parent table. Also, we can skip permission - * checks (this is necessary not just an optimization, else we'd fail to - * handle toast tables properly). - * - * recursing is also true if ALTER TYPE OWNER is calling us to fix up a - * free-standing composite type. - */ - void - ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lockmode) - { - Relation target_rel; - Relation class_rel; - HeapTuple tuple; - Form_pg_class tuple_class; ++ ScanKeyInit(&skey[0], ++ Anum_pg_constraint_conrelid, ++ BTEqualStrategyNumber, F_OIDEQ, ++ ObjectIdGetDatum(childrelid)); ++ ScanKeyInit(&skey[1], ++ Anum_pg_constraint_contypid, ++ BTEqualStrategyNumber, F_OIDEQ, ++ ObjectIdGetDatum(InvalidOid)); ++ ScanKeyInit(&skey[2], ++ Anum_pg_constraint_conname, ++ BTEqualStrategyNumber, F_NAMEEQ, ++ CStringGetDatum(constrName)); ++ scan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId, ++ true, NULL, 3, skey); ++ /* There can only be one, so no need to loop */ ++ tuple = systable_getnext(scan); ++ if (!HeapTupleIsValid(tuple)) ++ ereport(ERROR, ++ (errcode(ERRCODE_UNDEFINED_OBJECT), ++ errmsg("constraint \"%s\" of relation \"%s\" does not exist", ++ constrName, ++ RelationGetRelationName(childrel)))); ++ tuple = heap_copytuple(tuple); ++ systable_endscan(scan); ++ } + - /* - * Get exclusive lock till end of transaction on the target table. Use - * relation_open so that we can work on indexes and sequences. - */ - target_rel = relation_open(relationOid, lockmode); ++ childcon = (Form_pg_constraint) GETSTRUCT(tuple); + - /* Get its pg_class tuple, too */ - class_rel = table_open(RelationRelationId, RowExclusiveLock); ++ /* Right now only CHECK and not-null constraints can be inherited */ ++ if (childcon->contype != CONSTRAINT_CHECK && ++ childcon->contype != CONSTRAINT_NOTNULL) ++ elog(ERROR, "inherited constraint is not a CHECK or not-null constraint"); + - tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relationOid)); - if (!HeapTupleIsValid(tuple)) - elog(ERROR, "cache lookup failed for relation %u", relationOid); - tuple_class = (Form_pg_class) GETSTRUCT(tuple); ++ if (childcon->coninhcount <= 0) /* shouldn't happen */ ++ elog(ERROR, "relation %u has non-inherited constraint \"%s\"", ++ childrelid, NameStr(childcon->conname)); + - /* Can we change the ownership of this tuple? */ - switch (tuple_class->relkind) - { - case RELKIND_RELATION: - case RELKIND_VIEW: - case RELKIND_MATVIEW: - case RELKIND_FOREIGN_TABLE: - case RELKIND_PARTITIONED_TABLE: - /* ok to change owner */ - break; - case RELKIND_INDEX: - if (!recursing) ++ if (recurse) ++ { ++ /* ++ * If the child constraint has other definition sources, just ++ * decrement its inheritance count; if not, recurse to delete it. ++ */ ++ if (childcon->coninhcount == 1 && !childcon->conislocal) + { - /* - * Because ALTER INDEX OWNER used to be allowed, and in fact - * is generated by old versions of pg_dump, we give a warning - * and do nothing rather than erroring out. Also, to avoid - * unnecessary chatter while restoring those old dumps, say - * nothing at all if the command would be a no-op anyway. - */ - if (tuple_class->relowner != newOwnerId) - ereport(WARNING, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot change owner of index \"%s\"", - NameStr(tuple_class->relname)), - errhint("Change the ownership of the index's table instead."))); - /* quick hack to exit via the no-op path */ - newOwnerId = tuple_class->relowner; - } - break; - case RELKIND_PARTITIONED_INDEX: - if (recursing) - break; - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot change owner of index \"%s\"", - NameStr(tuple_class->relname)), - errhint("Change the ownership of the index's table instead."))); - break; - case RELKIND_SEQUENCE: - if (!recursing && - tuple_class->relowner != newOwnerId) - { - /* if it's an owned sequence, disallow changing it by itself */ - Oid tableId; - int32 colId; - - if (sequenceIsOwned(relationOid, DEPENDENCY_AUTO, &tableId, &colId) || - sequenceIsOwned(relationOid, DEPENDENCY_INTERNAL, &tableId, &colId)) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot change owner of sequence \"%s\"", - NameStr(tuple_class->relname)), - errdetail("Sequence \"%s\" is linked to table \"%s\".", - NameStr(tuple_class->relname), - get_rel_name(tableId)))); ++ /* Time to delete this child constraint, too */ ++ dropconstraint_internal(childrel, tuple, behavior, ++ recurse, true, missing_ok, ++ lockmode); + } - break; - case RELKIND_COMPOSITE_TYPE: - if (recursing) - break; - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is a composite type", - NameStr(tuple_class->relname)), - /* translator: %s is an SQL ALTER command */ - errhint("Use %s instead.", - "ALTER TYPE"))); - break; - case RELKIND_TOASTVALUE: - if (recursing) - break; - /* FALL THRU */ - default: - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot change owner of relation \"%s\"", - NameStr(tuple_class->relname)), - errdetail_relkind_not_supported(tuple_class->relkind))); - } - - /* - * If the new owner is the same as the existing owner, consider the - * command to have succeeded. This is for dump restoration purposes. - */ - if (tuple_class->relowner != newOwnerId) - { - Datum repl_val[Natts_pg_class]; - bool repl_null[Natts_pg_class]; - bool repl_repl[Natts_pg_class]; - Acl *newAcl; - Datum aclDatum; - bool isNull; - HeapTuple newtuple; - - /* skip permission checks when recursing to index or toast table */ - if (!recursing) - { - /* Superusers can always do it */ - if (!superuser()) ++ else + { - Oid namespaceOid = tuple_class->relnamespace; - AclResult aclresult; - - /* Otherwise, must be owner of the existing object */ - if (!object_ownercheck(RelationRelationId, relationOid, GetUserId())) - aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relationOid)), - RelationGetRelationName(target_rel)); - - /* Must be able to become new owner */ - check_can_set_role(GetUserId(), newOwnerId); ++ /* Child constraint must survive my deletion */ ++ childcon->coninhcount--; ++ CatalogTupleUpdate(conrel, &tuple->t_self, tuple); + - /* New owner must have CREATE privilege on namespace */ - aclresult = object_aclcheck(NamespaceRelationId, namespaceOid, newOwnerId, - ACL_CREATE); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_SCHEMA, - get_namespace_name(namespaceOid)); ++ /* Make update visible */ ++ CommandCounterIncrement(); + } + } - - memset(repl_null, false, sizeof(repl_null)); - memset(repl_repl, false, sizeof(repl_repl)); - - repl_repl[Anum_pg_class_relowner - 1] = true; - repl_val[Anum_pg_class_relowner - 1] = ObjectIdGetDatum(newOwnerId); - - /* - * Determine the modified ACL for the new owner. This is only - * necessary when the ACL is non-null. - */ - aclDatum = SysCacheGetAttr(RELOID, tuple, - Anum_pg_class_relacl, - &isNull); - if (!isNull) - { - newAcl = aclnewowner(DatumGetAclP(aclDatum), - tuple_class->relowner, newOwnerId); - repl_repl[Anum_pg_class_relacl - 1] = true; - repl_val[Anum_pg_class_relacl - 1] = PointerGetDatum(newAcl); - } - - newtuple = heap_modify_tuple(tuple, RelationGetDescr(class_rel), repl_val, repl_null, repl_repl); - - CatalogTupleUpdate(class_rel, &newtuple->t_self, newtuple); - - heap_freetuple(newtuple); - - /* - * We must similarly update any per-column ACLs to reflect the new - * owner; for neatness reasons that's split out as a subroutine. - */ - change_owner_fix_column_acls(relationOid, - tuple_class->relowner, - newOwnerId); - - /* - * Update owner dependency reference, if any. A composite type has - * none, because it's tracked for the pg_type entry instead of here; - * indexes and TOAST tables don't have their own entries either. - */ - if (tuple_class->relkind != RELKIND_COMPOSITE_TYPE && - tuple_class->relkind != RELKIND_INDEX && - tuple_class->relkind != RELKIND_PARTITIONED_INDEX && - tuple_class->relkind != RELKIND_TOASTVALUE) - changeDependencyOnOwner(RelationRelationId, relationOid, - newOwnerId); - - /* - * Also change the ownership of the table's row type, if it has one - */ - if (OidIsValid(tuple_class->reltype)) - AlterTypeOwnerInternal(tuple_class->reltype, newOwnerId); - - /* - * If we are operating on a table or materialized view, also change - * the ownership of any indexes and sequences that belong to the - * relation, as well as its toast table (if it has one). - */ - if (tuple_class->relkind == RELKIND_RELATION || - tuple_class->relkind == RELKIND_PARTITIONED_TABLE || - tuple_class->relkind == RELKIND_MATVIEW || - tuple_class->relkind == RELKIND_TOASTVALUE) ++ else + { - List *index_oid_list; - ListCell *i; - - /* Find all the indexes belonging to this relation */ - index_oid_list = RelationGetIndexList(target_rel); ++ /* ++ * If we were told to drop ONLY in this table (no recursion) and ++ * there are no further parents for this constraint, we need to ++ * mark the inheritors' constraints as locally defined rather than ++ * inherited. ++ */ ++ childcon->coninhcount--; ++ if (childcon->coninhcount == 0) ++ childcon->conislocal = true; + - /* For each index, recursively change its ownership */ - foreach(i, index_oid_list) - ATExecChangeOwner(lfirst_oid(i), newOwnerId, true, lockmode); ++ CatalogTupleUpdate(conrel, &tuple->t_self, tuple); + - list_free(index_oid_list); ++ /* Make update visible */ ++ CommandCounterIncrement(); + } + - /* If it has a toast table, recurse to change its ownership */ - if (tuple_class->reltoastrelid != InvalidOid) - ATExecChangeOwner(tuple_class->reltoastrelid, newOwnerId, - true, lockmode); ++ heap_freetuple(tuple); + - /* If it has dependent sequences, recurse to change them too */ - change_owner_recurse_to_sequences(relationOid, newOwnerId, lockmode); ++ table_close(childrel, NoLock); + } + - InvokeObjectPostAlterHook(RelationRelationId, relationOid, 0); ++ table_close(conrel, RowExclusiveLock); + - ReleaseSysCache(tuple); - table_close(class_rel, RowExclusiveLock); - relation_close(target_rel, NoLock); ++ return conobj; +} + +/* - * change_owner_fix_column_acls ++ * ALTER COLUMN TYPE + * - * Helper function for ATExecChangeOwner. Scan the columns of the table - * and fix any non-null column ACLs to reflect the new owner. ++ * Unlike other subcommand types, we do parse transformation for ALTER COLUMN ++ * TYPE during phase 1 --- the AlterTableCmd passed in here is already ++ * transformed (and must be, because we rely on some transformed fields). ++ * ++ * The point of this is that the execution of all ALTER COLUMN TYPEs for a ++ * table will be done "in parallel" during phase 3, so all the USING ++ * expressions should be parsed assuming the original column types. Also, ++ * this allows a USING expression to refer to a field that will be dropped. ++ * ++ * To make this work safely, AT_PASS_DROP then AT_PASS_ALTER_TYPE must be ++ * the first two execution steps in phase 2; they must not see the effects ++ * of any other subcommand types, since the USING expressions are parsed ++ * against the unmodified table's state. + */ +static void - change_owner_fix_column_acls(Oid relationOid, Oid oldOwnerId, Oid newOwnerId) ++ATPrepAlterColumnType(List **wqueue, ++ AlteredTableInfo *tab, Relation rel, ++ bool recurse, bool recursing, ++ AlterTableCmd *cmd, LOCKMODE lockmode, ++ AlterTableUtilityContext *context) +{ - Relation attRelation; - SysScanDesc scan; - ScanKeyData key[1]; - HeapTuple attributeTuple; ++ char *colName = cmd->name; ++ ColumnDef *def = (ColumnDef *) cmd->def; ++ TypeName *typeName = def->typeName; ++ Node *transform = def->cooked_default; ++ HeapTuple tuple; ++ Form_pg_attribute attTup; ++ AttrNumber attnum; ++ Oid targettype; ++ int32 targettypmod; ++ Oid targetcollid; ++ NewColumnValue *newval; ++ ParseState *pstate = make_parsestate(NULL); ++ AclResult aclresult; ++ bool is_expr; + - attRelation = table_open(AttributeRelationId, RowExclusiveLock); - ScanKeyInit(&key[0], - Anum_pg_attribute_attrelid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(relationOid)); - scan = systable_beginscan(attRelation, AttributeRelidNumIndexId, - true, NULL, 1, key); - while (HeapTupleIsValid(attributeTuple = systable_getnext(scan))) - { - Form_pg_attribute att = (Form_pg_attribute) GETSTRUCT(attributeTuple); - Datum repl_val[Natts_pg_attribute]; - bool repl_null[Natts_pg_attribute]; - bool repl_repl[Natts_pg_attribute]; - Acl *newAcl; - Datum aclDatum; - bool isNull; - HeapTuple newtuple; - - /* Ignore dropped columns */ - if (att->attisdropped) - continue; - - aclDatum = heap_getattr(attributeTuple, - Anum_pg_attribute_attacl, - RelationGetDescr(attRelation), - &isNull); - /* Null ACLs do not require changes */ - if (isNull) - continue; - - memset(repl_null, false, sizeof(repl_null)); - memset(repl_repl, false, sizeof(repl_repl)); - - newAcl = aclnewowner(DatumGetAclP(aclDatum), - oldOwnerId, newOwnerId); - repl_repl[Anum_pg_attribute_attacl - 1] = true; - repl_val[Anum_pg_attribute_attacl - 1] = PointerGetDatum(newAcl); ++ pstate->p_sourcetext = context->queryString; + - newtuple = heap_modify_tuple(attributeTuple, - RelationGetDescr(attRelation), - repl_val, repl_null, repl_repl); ++ if (rel->rd_rel->reloftype && !recursing) ++ ereport(ERROR, ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("cannot alter column type of typed table"), ++ parser_errposition(pstate, def->location))); + - CatalogTupleUpdate(attRelation, &newtuple->t_self, newtuple); ++ /* lookup the attribute so we can check inheritance status */ ++ tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName); ++ if (!HeapTupleIsValid(tuple)) ++ ereport(ERROR, ++ (errcode(ERRCODE_UNDEFINED_COLUMN), ++ errmsg("column \"%s\" of relation \"%s\" does not exist", ++ colName, RelationGetRelationName(rel)), ++ parser_errposition(pstate, def->location))); ++ attTup = (Form_pg_attribute) GETSTRUCT(tuple); ++ attnum = attTup->attnum; + - heap_freetuple(newtuple); - } - systable_endscan(scan); - table_close(attRelation, RowExclusiveLock); - } ++ /* Can't alter a system attribute */ ++ if (attnum <= 0) ++ ereport(ERROR, ++ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ++ errmsg("cannot alter system column \"%s\"", colName), ++ parser_errposition(pstate, def->location))); + - /* - * change_owner_recurse_to_sequences - * - * Helper function for ATExecChangeOwner. Examines pg_depend searching - * for sequences that are dependent on serial columns, and changes their - * ownership. - */ - static void - change_owner_recurse_to_sequences(Oid relationOid, Oid newOwnerId, LOCKMODE lockmode) - { - Relation depRel; - SysScanDesc scan; - ScanKeyData key[2]; - HeapTuple tup; ++ /* ++ * Cannot specify USING when altering type of a generated column, because ++ * that would violate the generation expression. ++ */ ++ if (attTup->attgenerated && def->cooked_default) ++ ereport(ERROR, ++ (errcode(ERRCODE_INVALID_COLUMN_DEFINITION), ++ errmsg("cannot specify USING when altering type of generated column"), ++ errdetail("Column \"%s\" is a generated column.", colName), ++ parser_errposition(pstate, def->location))); + + /* - * SERIAL sequences are those having an auto dependency on one of the - * table's columns (we don't care *which* column, exactly). ++ * Don't alter inherited columns. At outer level, there had better not be ++ * any inherited definition; when recursing, we assume this was checked at ++ * the parent level (see below). + */ - depRel = table_open(DependRelationId, AccessShareLock); ++ if (attTup->attinhcount > 0 && !recursing) ++ ereport(ERROR, ++ (errcode(ERRCODE_INVALID_TABLE_DEFINITION), ++ errmsg("cannot alter inherited column \"%s\"", colName), ++ parser_errposition(pstate, def->location))); + - ScanKeyInit(&key[0], - Anum_pg_depend_refclassid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(RelationRelationId)); - ScanKeyInit(&key[1], - Anum_pg_depend_refobjid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(relationOid)); - /* we leave refobjsubid unspecified */ ++ /* Don't alter columns used in the partition key */ ++ if (has_partition_attrs(rel, ++ bms_make_singleton(attnum - FirstLowInvalidHeapAttributeNumber), ++ &is_expr)) ++ ereport(ERROR, ++ (errcode(ERRCODE_INVALID_TABLE_DEFINITION), ++ errmsg("cannot alter column \"%s\" because it is part of the partition key of relation \"%s\"", ++ colName, RelationGetRelationName(rel)), ++ parser_errposition(pstate, def->location))); + - scan = systable_beginscan(depRel, DependReferenceIndexId, true, - NULL, 2, key); ++ /* Look up the target type */ ++ typenameTypeIdAndMod(pstate, typeName, &targettype, &targettypmod); + - while (HeapTupleIsValid(tup = systable_getnext(scan))) - { - Form_pg_depend depForm = (Form_pg_depend) GETSTRUCT(tup); - Relation seqRel; ++ aclresult = object_aclcheck(TypeRelationId, targettype, GetUserId(), ACL_USAGE); ++ if (aclresult != ACLCHECK_OK) ++ aclcheck_error_type(aclresult, targettype); + - /* 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; ++ /* And the collation */ ++ targetcollid = GetColumnDefCollation(pstate, def, targettype); + - /* Use relation_open just in case it's an index */ - seqRel = relation_open(depForm->objid, lockmode); ++ /* make sure datatype is legal for a column */ ++ CheckAttributeType(colName, targettype, targetcollid, ++ list_make1_oid(rel->rd_rel->reltype), ++ (attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL ? CHKATYPE_IS_VIRTUAL : 0)); + - /* skip non-sequence relations */ - if (RelationGetForm(seqRel)->relkind != RELKIND_SEQUENCE) ++ if (attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) ++ { ++ /* do nothing */ ++ } ++ else if (tab->relkind == RELKIND_RELATION || ++ tab->relkind == RELKIND_PARTITIONED_TABLE) ++ { ++ /* ++ * Set up an expression to transform the old data value to the new ++ * type. If a USING option was given, use the expression as ++ * transformed by transformAlterTableStmt, else just take the old ++ * value and try to coerce it. We do this first so that type ++ * incompatibility can be detected before we waste effort, and because ++ * we need the expression to be parsed against the original table row ++ * type. ++ */ ++ if (!transform) + { - /* No need to keep the lock */ - relation_close(seqRel, lockmode); - continue; ++ transform = (Node *) makeVar(1, attnum, ++ attTup->atttypid, attTup->atttypmod, ++ attTup->attcollation, ++ 0); + } + - /* We don't need to close the sequence while we alter it. */ - ATExecChangeOwner(depForm->objid, newOwnerId, true, lockmode); - - /* Now we can close it. Keep the lock till end of transaction. */ - relation_close(seqRel, NoLock); - } ++ transform = coerce_to_target_type(pstate, ++ transform, exprType(transform), ++ targettype, targettypmod, ++ COERCION_ASSIGNMENT, ++ COERCE_IMPLICIT_CAST, ++ -1); ++ if (transform == NULL) ++ { ++ /* error text depends on whether USING was specified or not */ ++ if (def->cooked_default != NULL) ++ ereport(ERROR, ++ (errcode(ERRCODE_DATATYPE_MISMATCH), ++ errmsg("result of USING clause for column \"%s\"" ++ " cannot be cast automatically to type %s", ++ colName, format_type_be(targettype)), ++ errhint("You might need to add an explicit cast."))); ++ else ++ ereport(ERROR, ++ (errcode(ERRCODE_DATATYPE_MISMATCH), ++ errmsg("column \"%s\" cannot be cast automatically to type %s", ++ colName, format_type_be(targettype)), ++ !attTup->attgenerated ? ++ /* translator: USING is SQL, don't translate it */ ++ errhint("You might need to specify \"USING %s::%s\".", ++ quote_identifier(colName), ++ format_type_with_typemod(targettype, ++ targettypmod)) : 0)); ++ } + - systable_endscan(scan); ++ /* Fix collations after all else */ ++ assign_expr_collations(pstate, transform); + - relation_close(depRel, AccessShareLock); - } ++ /* Expand virtual generated columns in the expr. */ ++ transform = expand_generated_columns_in_expr(transform, rel, 1); + - /* - * ALTER TABLE CLUSTER ON - * - * The only thing we have to do is to change the indisclustered bits. - * - * Return the address of the new clustering index. - */ - static ObjectAddress - ATExecClusterOn(Relation rel, const char *indexName, LOCKMODE lockmode) - { - Oid indexOid; - ObjectAddress address; ++ /* Plan the expr now so we can accurately assess the need to rewrite. */ ++ transform = (Node *) expression_planner((Expr *) transform); + - indexOid = get_relname_relid(indexName, rel->rd_rel->relnamespace); ++ /* ++ * Add a work queue item to make ATRewriteTable update the column ++ * contents. ++ */ ++ newval = (NewColumnValue *) palloc0(sizeof(NewColumnValue)); ++ newval->attnum = attnum; ++ newval->expr = (Expr *) transform; ++ newval->is_generated = false; + - if (!OidIsValid(indexOid)) ++ tab->newvals = lappend(tab->newvals, newval); ++ if (ATColumnChangeRequiresRewrite(transform, attnum)) ++ tab->rewrite |= AT_REWRITE_COLUMN_REWRITE; ++ } ++ else if (transform) + ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("index \"%s\" for table \"%s\" does not exist", - indexName, RelationGetRelationName(rel)))); ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("\"%s\" is not a table", ++ RelationGetRelationName(rel)))); + - /* Check index is valid to cluster on */ - check_index_is_clusterable(rel, indexOid, lockmode); ++ if (!RELKIND_HAS_STORAGE(tab->relkind) || attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) ++ { ++ /* ++ * For relations or columns without storage, do this check now. ++ * Regular tables will check it later when the table is being ++ * rewritten. ++ */ ++ find_composite_type_dependencies(rel->rd_rel->reltype, rel, NULL); ++ } + - /* And do the work */ - mark_index_clustered(rel, indexOid, false); ++ ReleaseSysCache(tuple); + - ObjectAddressSet(address, - RelationRelationId, indexOid); ++ /* ++ * Recurse manually by queueing a new command for each child, if ++ * necessary. We cannot apply ATSimpleRecursion here because we need to ++ * remap attribute numbers in the USING expression, if any. ++ * ++ * If we are told not to recurse, there had better not be any child ++ * tables; else the alter would put them out of step. ++ */ ++ if (recurse) ++ { ++ Oid relid = RelationGetRelid(rel); ++ List *child_oids, ++ *child_numparents; ++ ListCell *lo, ++ *li; + - return address; - } ++ child_oids = find_all_inheritors(relid, lockmode, ++ &child_numparents); + - /* - * ALTER TABLE SET WITHOUT CLUSTER - * - * We have to find any indexes on the table that have indisclustered bit - * set and turn it off. - */ - static void - ATExecDropCluster(Relation rel, LOCKMODE lockmode) - { - mark_index_clustered(rel, InvalidOid, false); ++ /* ++ * find_all_inheritors does the recursive search of the inheritance ++ * hierarchy, so all we have to do is process all of the relids in the ++ * list that it returns. ++ */ ++ forboth(lo, child_oids, li, child_numparents) ++ { ++ Oid childrelid = lfirst_oid(lo); ++ int numparents = lfirst_int(li); ++ Relation childrel; ++ HeapTuple childtuple; ++ Form_pg_attribute childattTup; ++ ++ if (childrelid == relid) ++ continue; ++ ++ /* find_all_inheritors already got lock */ ++ childrel = relation_open(childrelid, NoLock); ++ CheckAlterTableIsSafe(childrel); ++ ++ /* ++ * Verify that the child doesn't have any inherited definitions of ++ * this column that came from outside this inheritance hierarchy. ++ * (renameatt makes a similar test, though in a different way ++ * because of its different recursion mechanism.) ++ */ ++ childtuple = SearchSysCacheAttName(RelationGetRelid(childrel), ++ colName); ++ if (!HeapTupleIsValid(childtuple)) ++ ereport(ERROR, ++ (errcode(ERRCODE_UNDEFINED_COLUMN), ++ errmsg("column \"%s\" of relation \"%s\" does not exist", ++ colName, RelationGetRelationName(childrel)))); ++ childattTup = (Form_pg_attribute) GETSTRUCT(childtuple); ++ ++ if (childattTup->attinhcount > numparents) ++ ereport(ERROR, ++ (errcode(ERRCODE_INVALID_TABLE_DEFINITION), ++ errmsg("cannot alter inherited column \"%s\" of relation \"%s\"", ++ colName, RelationGetRelationName(childrel)))); ++ ++ ReleaseSysCache(childtuple); ++ ++ /* ++ * Remap the attribute numbers. If no USING expression was ++ * specified, there is no need for this step. ++ */ ++ if (def->cooked_default) ++ { ++ AttrMap *attmap; ++ bool found_whole_row; ++ ++ /* create a copy to scribble on */ ++ cmd = copyObject(cmd); ++ ++ attmap = build_attrmap_by_name(RelationGetDescr(childrel), ++ RelationGetDescr(rel), ++ false); ++ ((ColumnDef *) cmd->def)->cooked_default = ++ map_variable_attnos(def->cooked_default, ++ 1, 0, ++ attmap, ++ InvalidOid, &found_whole_row); ++ if (found_whole_row) ++ ereport(ERROR, ++ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ++ errmsg("cannot convert whole-row table reference"), ++ errdetail("USING expression contains a whole-row table reference."))); ++ pfree(attmap); ++ } ++ ATPrepCmd(wqueue, childrel, cmd, false, true, lockmode, context); ++ relation_close(childrel, NoLock); ++ } ++ } ++ else if (!recursing && ++ find_inheritance_children(RelationGetRelid(rel), NoLock) != NIL) ++ ereport(ERROR, ++ (errcode(ERRCODE_INVALID_TABLE_DEFINITION), ++ errmsg("type of inherited column \"%s\" must be changed in child tables too", ++ colName))); ++ ++ if (tab->relkind == RELKIND_COMPOSITE_TYPE) ++ ATTypedTableRecursion(wqueue, rel, cmd, lockmode, context); +} + +/* - * Preparation phase for SET ACCESS METHOD ++ * When the data type of a column is changed, a rewrite might not be required ++ * if the new type is sufficiently identical to the old one, and the USING ++ * clause isn't trying to insert some other value. It's safe to skip the ++ * rewrite in these cases: + * - * Check that the access method exists and determine whether a change is - * actually needed. ++ * - the old type is binary coercible to the new type ++ * - the new type is an unconstrained domain over the old type ++ * - {NEW,OLD} or {OLD,NEW} is {timestamptz,timestamp} and the timezone is UTC ++ * ++ * In the case of a constrained domain, we could get by with scanning the ++ * table and checking the constraint rather than actually rewriting it, but we ++ * don't currently try to do that. + */ - static void - ATPrepSetAccessMethod(AlteredTableInfo *tab, Relation rel, const char *amname) ++static bool ++ATColumnChangeRequiresRewrite(Node *expr, AttrNumber varattno) +{ - Oid amoid; ++ Assert(expr != NULL); + - /* - * Look up the access method name and check that it differs from the - * table's current AM. If DEFAULT was specified for a partitioned table - * (amname is NULL), set it to InvalidOid to reset the catalogued AM. - */ - if (amname != NULL) - amoid = get_table_am_oid(amname, false); - else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) - amoid = InvalidOid; - else - amoid = get_table_am_oid(default_table_access_method, false); ++ for (;;) ++ { ++ /* only one varno, so no need to check that */ ++ if (IsA(expr, Var) && ((Var *) expr)->varattno == varattno) ++ return false; ++ else if (IsA(expr, RelabelType)) ++ expr = (Node *) ((RelabelType *) expr)->arg; ++ else if (IsA(expr, CoerceToDomain)) ++ { ++ CoerceToDomain *d = (CoerceToDomain *) expr; + - /* if it's a match, phase 3 doesn't need to do anything */ - if (rel->rd_rel->relam == amoid) - return; ++ if (DomainHasConstraints(d->resulttype)) ++ return true; ++ expr = (Node *) d->arg; ++ } ++ else if (IsA(expr, FuncExpr)) ++ { ++ FuncExpr *f = (FuncExpr *) expr; + - /* Save info for Phase 3 to do the real work */ - tab->rewrite |= AT_REWRITE_ACCESS_METHOD; - tab->newAccessMethod = amoid; - tab->chgAccessMethod = true; ++ switch (f->funcid) ++ { ++ case F_TIMESTAMPTZ_TIMESTAMP: ++ case F_TIMESTAMP_TIMESTAMPTZ: ++ if (TimestampTimestampTzRequiresRewrite()) ++ return true; ++ else ++ expr = linitial(f->args); ++ break; ++ default: ++ return true; ++ } ++ } ++ else ++ return true; ++ } +} + +/* - * Special handling of ALTER TABLE SET ACCESS METHOD for relations with no - * storage that have an interest in preserving AM. ++ * ALTER COLUMN .. SET DATA TYPE + * - * Since these have no storage, setting the access method is a catalog only - * operation. ++ * Return the address of the modified column. + */ - static void - ATExecSetAccessMethodNoStorage(Relation rel, Oid newAccessMethodId) ++static ObjectAddress ++ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, ++ AlterTableCmd *cmd, LOCKMODE lockmode) +{ - Relation pg_class; - Oid oldAccessMethodId; - HeapTuple tuple; - Form_pg_class rd_rel; - Oid reloid = RelationGetRelid(rel); ++ char *colName = cmd->name; ++ ColumnDef *def = (ColumnDef *) cmd->def; ++ TypeName *typeName = def->typeName; ++ HeapTuple heapTup; ++ Form_pg_attribute attTup, ++ attOldTup; ++ AttrNumber attnum; ++ HeapTuple typeTuple; ++ Form_pg_type tform; ++ Oid targettype; ++ int32 targettypmod; ++ Oid targetcollid; ++ Node *defaultexpr; ++ Relation attrelation; ++ Relation depRel; ++ ScanKeyData key[3]; ++ SysScanDesc scan; ++ HeapTuple depTup; ++ ObjectAddress address; + + /* - * Shouldn't be called on relations having storage; these are processed in - * phase 3. ++ * Clear all the missing values if we're rewriting the table, since this ++ * renders them pointless. + */ - Assert(!RELKIND_HAS_STORAGE(rel->rd_rel->relkind)); - - /* Get a modifiable copy of the relation's pg_class row. */ - pg_class = table_open(RelationRelationId, RowExclusiveLock); - - tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid)); - if (!HeapTupleIsValid(tuple)) - elog(ERROR, "cache lookup failed for relation %u", reloid); - rd_rel = (Form_pg_class) GETSTRUCT(tuple); - - /* Update the pg_class row. */ - oldAccessMethodId = rd_rel->relam; - rd_rel->relam = newAccessMethodId; - - /* Leave if no update required */ - if (rd_rel->relam == oldAccessMethodId) ++ if (tab->rewrite) + { - heap_freetuple(tuple); - table_close(pg_class, RowExclusiveLock); - return; ++ Relation newrel; ++ ++ newrel = table_open(RelationGetRelid(rel), NoLock); ++ RelationClearMissing(newrel); ++ relation_close(newrel, NoLock); ++ /* make sure we don't conflict with later attribute modifications */ ++ CommandCounterIncrement(); + } + - CatalogTupleUpdate(pg_class, &tuple->t_self, tuple); ++ attrelation = table_open(AttributeRelationId, RowExclusiveLock); + - /* - * Update the dependency on the new access method. No dependency is added - * if the new access method is InvalidOid (default case). Be very careful - * that this has to compare the previous value stored in pg_class with the - * new one. - */ - if (!OidIsValid(oldAccessMethodId) && OidIsValid(rd_rel->relam)) - { - ObjectAddress relobj, - referenced; ++ /* Look up the target column */ ++ heapTup = SearchSysCacheCopyAttName(RelationGetRelid(rel), colName); ++ if (!HeapTupleIsValid(heapTup)) /* shouldn't happen */ ++ ereport(ERROR, ++ (errcode(ERRCODE_UNDEFINED_COLUMN), ++ errmsg("column \"%s\" of relation \"%s\" does not exist", ++ colName, RelationGetRelationName(rel)))); ++ attTup = (Form_pg_attribute) GETSTRUCT(heapTup); ++ attnum = attTup->attnum; ++ attOldTup = TupleDescAttr(tab->oldDesc, attnum - 1); + - /* - * New access method is defined and there was no dependency - * previously, so record a new one. - */ - ObjectAddressSet(relobj, RelationRelationId, reloid); - ObjectAddressSet(referenced, AccessMethodRelationId, rd_rel->relam); - recordDependencyOn(&relobj, &referenced, DEPENDENCY_NORMAL); - } - else if (OidIsValid(oldAccessMethodId) && - !OidIsValid(rd_rel->relam)) ++ /* Check for multiple ALTER TYPE on same column --- can't cope */ ++ if (attTup->atttypid != attOldTup->atttypid || ++ attTup->atttypmod != attOldTup->atttypmod) ++ ereport(ERROR, ++ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ++ errmsg("cannot alter type of column \"%s\" twice", ++ colName))); ++ ++ /* Look up the target type (should not fail, since prep found it) */ ++ typeTuple = typenameType(NULL, typeName, &targettypmod); ++ tform = (Form_pg_type) GETSTRUCT(typeTuple); ++ targettype = tform->oid; ++ /* And the collation */ ++ targetcollid = GetColumnDefCollation(NULL, def, targettype); ++ ++ /* ++ * If there is a default expression for the column, get it and ensure we ++ * can coerce it to the new datatype. (We must do this before changing ++ * the column type, because build_column_default itself will try to ++ * coerce, and will not issue the error message we want if it fails.) ++ * ++ * We remove any implicit coercion steps at the top level of the old ++ * default expression; this has been agreed to satisfy the principle of ++ * least surprise. (The conversion to the new column type should act like ++ * it started from what the user sees as the stored expression, and the ++ * implicit coercions aren't going to be shown.) ++ */ ++ if (attTup->atthasdef) + { - /* - * There was an access method defined, and no new one, so just remove - * the existing dependency. - */ - deleteDependencyRecordsForClass(RelationRelationId, reloid, - AccessMethodRelationId, - DEPENDENCY_NORMAL); ++ defaultexpr = build_column_default(rel, attnum); ++ Assert(defaultexpr); ++ defaultexpr = strip_implicit_coercions(defaultexpr); ++ defaultexpr = coerce_to_target_type(NULL, /* no UNKNOWN params */ ++ defaultexpr, exprType(defaultexpr), ++ targettype, targettypmod, ++ COERCION_ASSIGNMENT, ++ COERCE_IMPLICIT_CAST, ++ -1); ++ if (defaultexpr == NULL) ++ { ++ if (attTup->attgenerated) ++ ereport(ERROR, ++ (errcode(ERRCODE_DATATYPE_MISMATCH), ++ errmsg("generation expression for column \"%s\" cannot be cast automatically to type %s", ++ colName, format_type_be(targettype)))); ++ else ++ ereport(ERROR, ++ (errcode(ERRCODE_DATATYPE_MISMATCH), ++ errmsg("default for column \"%s\" cannot be cast automatically to type %s", ++ colName, format_type_be(targettype)))); ++ } + } + else - { - Assert(OidIsValid(oldAccessMethodId) && - OidIsValid(rd_rel->relam)); ++ defaultexpr = NULL; + - /* Both are valid, so update the dependency */ - changeDependencyFor(RelationRelationId, reloid, - AccessMethodRelationId, - oldAccessMethodId, rd_rel->relam); - } ++ /* ++ * Find everything that depends on the column (constraints, indexes, etc), ++ * and record enough information to let us recreate the objects. ++ * ++ * The actual recreation does not happen here, but only after we have ++ * performed all the individual ALTER TYPE operations. We have to save ++ * the info before executing ALTER TYPE, though, else the deparser will ++ * get confused. ++ */ ++ RememberAllDependentForRebuilding(tab, AT_AlterColumnType, rel, attnum, colName); + - /* make the relam and dependency changes visible */ - CommandCounterIncrement(); ++ /* ++ * Now scan for dependencies of this column on other things. The only ++ * things we should find are the dependency on the column datatype and ++ * possibly a collation dependency. Those can be removed. ++ */ ++ depRel = table_open(DependRelationId, RowExclusiveLock); + - InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0); ++ ScanKeyInit(&key[0], ++ Anum_pg_depend_classid, ++ BTEqualStrategyNumber, F_OIDEQ, ++ ObjectIdGetDatum(RelationRelationId)); ++ ScanKeyInit(&key[1], ++ Anum_pg_depend_objid, ++ BTEqualStrategyNumber, F_OIDEQ, ++ ObjectIdGetDatum(RelationGetRelid(rel))); ++ ScanKeyInit(&key[2], ++ Anum_pg_depend_objsubid, ++ BTEqualStrategyNumber, F_INT4EQ, ++ Int32GetDatum((int32) attnum)); + - heap_freetuple(tuple); - table_close(pg_class, RowExclusiveLock); - } ++ scan = systable_beginscan(depRel, DependDependerIndexId, true, ++ NULL, 3, key); + - /* - * ALTER TABLE SET TABLESPACE - */ - static void - ATPrepSetTableSpace(AlteredTableInfo *tab, Relation rel, const char *tablespacename, LOCKMODE lockmode) - { - Oid tablespaceId; ++ while (HeapTupleIsValid(depTup = systable_getnext(scan))) ++ { ++ Form_pg_depend foundDep = (Form_pg_depend) GETSTRUCT(depTup); ++ ObjectAddress foundObject; + - /* Check that the tablespace exists */ - tablespaceId = get_tablespace_oid(tablespacename, false); ++ foundObject.classId = foundDep->refclassid; ++ foundObject.objectId = foundDep->refobjid; ++ foundObject.objectSubId = foundDep->refobjsubid; + - /* Check permissions except when moving to database's default */ - if (OidIsValid(tablespaceId) && tablespaceId != MyDatabaseTableSpace) - { - AclResult aclresult; ++ if (foundDep->deptype != DEPENDENCY_NORMAL) ++ elog(ERROR, "found unexpected dependency type '%c'", ++ foundDep->deptype); ++ if (!(foundDep->refclassid == TypeRelationId && ++ foundDep->refobjid == attTup->atttypid) && ++ !(foundDep->refclassid == CollationRelationId && ++ foundDep->refobjid == attTup->attcollation)) ++ elog(ERROR, "found unexpected dependency for column: %s", ++ getObjectDescription(&foundObject, false)); + - aclresult = object_aclcheck(TableSpaceRelationId, tablespaceId, GetUserId(), ACL_CREATE); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_TABLESPACE, tablespacename); ++ CatalogTupleDelete(depRel, &depTup->t_self); + } + - /* Save info for Phase 3 to do the real work */ - if (OidIsValid(tab->newTableSpace)) - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("cannot have multiple SET TABLESPACE subcommands"))); - - tab->newTableSpace = tablespaceId; - } ++ systable_endscan(scan); + - /* - * Set, reset, or replace reloptions. - */ - static void - ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation, - LOCKMODE lockmode) - { - Oid relid; - Relation pgclass; - HeapTuple tuple; - HeapTuple newtuple; - Datum datum; - Datum newOptions; - Datum repl_val[Natts_pg_class]; - bool repl_null[Natts_pg_class]; - bool repl_repl[Natts_pg_class]; - const char *const validnsps[] = HEAP_RELOPT_NAMESPACES; ++ table_close(depRel, RowExclusiveLock); + - if (defList == NIL && operation != AT_ReplaceRelOptions) - return; /* nothing to do */ ++ /* ++ * Here we go --- change the recorded column type and collation. (Note ++ * heapTup is a copy of the syscache entry, so okay to scribble on.) First ++ * fix up the missing value if any. ++ */ ++ if (attTup->atthasmissing) ++ { ++ Datum missingval; ++ bool missingNull; + - pgclass = table_open(RelationRelationId, RowExclusiveLock); ++ /* if rewrite is true the missing value should already be cleared */ ++ Assert(tab->rewrite == 0); + - /* Fetch heap tuple */ - relid = RelationGetRelid(rel); - tuple = SearchSysCacheLocked1(RELOID, ObjectIdGetDatum(relid)); - if (!HeapTupleIsValid(tuple)) - elog(ERROR, "cache lookup failed for relation %u", relid); ++ /* Get the missing value datum */ ++ missingval = heap_getattr(heapTup, ++ Anum_pg_attribute_attmissingval, ++ attrelation->rd_att, ++ &missingNull); + - if (operation == AT_ReplaceRelOptions) - { - /* - * If we're supposed to replace the reloptions list, we just pretend - * there were none before. - */ - datum = (Datum) 0; - } - else - { - bool isnull; ++ /* if it's a null array there is nothing to do */ + - /* Get the old reloptions */ - datum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, - &isnull); - if (isnull) - datum = (Datum) 0; - } ++ if (!missingNull) ++ { ++ /* ++ * Get the datum out of the array and repack it in a new array ++ * built with the new type data. We assume that since the table ++ * doesn't need rewriting, the actual Datum doesn't need to be ++ * changed, only the array metadata. ++ */ + - /* Generate new proposed reloptions (text array) */ - newOptions = transformRelOptions(datum, defList, NULL, validnsps, false, - operation == AT_ResetRelOptions); ++ int one = 1; ++ bool isNull; ++ Datum valuesAtt[Natts_pg_attribute] = {0}; ++ bool nullsAtt[Natts_pg_attribute] = {0}; ++ bool replacesAtt[Natts_pg_attribute] = {0}; ++ HeapTuple newTup; + - /* Validate */ - switch (rel->rd_rel->relkind) - { - case RELKIND_RELATION: - case RELKIND_MATVIEW: - (void) heap_reloptions(rel->rd_rel->relkind, newOptions, true); - break; - case RELKIND_PARTITIONED_TABLE: - (void) partitioned_table_reloptions(newOptions, true); - break; - case RELKIND_VIEW: - (void) view_reloptions(newOptions, true); - break; - case RELKIND_INDEX: - case RELKIND_PARTITIONED_INDEX: - (void) index_reloptions(rel->rd_indam->amoptions, newOptions, true); - break; - case RELKIND_TOASTVALUE: - /* fall through to error -- shouldn't ever get here */ - default: - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot set options for relation \"%s\"", - RelationGetRelationName(rel)), - errdetail_relkind_not_supported(rel->rd_rel->relkind))); - break; - } - - /* Special-case validation of view options */ - if (rel->rd_rel->relkind == RELKIND_VIEW) - { - Query *view_query = get_view_query(rel); - List *view_options = untransformRelOptions(newOptions); - ListCell *cell; - bool check_option = false; - - foreach(cell, view_options) - { - DefElem *defel = (DefElem *) lfirst(cell); - - if (strcmp(defel->defname, "check_option") == 0) - check_option = true; - } ++ missingval = array_get_element(missingval, ++ 1, ++ &one, ++ 0, ++ attTup->attlen, ++ attTup->attbyval, ++ attTup->attalign, ++ &isNull); ++ missingval = PointerGetDatum(construct_array(&missingval, ++ 1, ++ targettype, ++ tform->typlen, ++ tform->typbyval, ++ tform->typalign)); + - /* - * If the check option is specified, look to see if the view is - * actually auto-updatable or not. - */ - if (check_option) - { - const char *view_updatable_error = - view_query_is_auto_updatable(view_query, true); ++ valuesAtt[Anum_pg_attribute_attmissingval - 1] = missingval; ++ replacesAtt[Anum_pg_attribute_attmissingval - 1] = true; ++ nullsAtt[Anum_pg_attribute_attmissingval - 1] = false; + - if (view_updatable_error) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("WITH CHECK OPTION is supported only on automatically updatable views"), - errhint("%s", _(view_updatable_error)))); ++ newTup = heap_modify_tuple(heapTup, RelationGetDescr(attrelation), ++ valuesAtt, nullsAtt, replacesAtt); ++ heap_freetuple(heapTup); ++ heapTup = newTup; ++ attTup = (Form_pg_attribute) GETSTRUCT(heapTup); + } + } + - /* - * All we need do here is update the pg_class row; the new options will be - * propagated into relcaches during post-commit cache inval. - */ - memset(repl_val, 0, sizeof(repl_val)); - memset(repl_null, false, sizeof(repl_null)); - memset(repl_repl, false, sizeof(repl_repl)); - - if (newOptions != (Datum) 0) - repl_val[Anum_pg_class_reloptions - 1] = newOptions; - else - repl_null[Anum_pg_class_reloptions - 1] = true; ++ attTup->atttypid = targettype; ++ attTup->atttypmod = targettypmod; ++ attTup->attcollation = targetcollid; ++ if (list_length(typeName->arrayBounds) > PG_INT16_MAX) ++ ereport(ERROR, ++ errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), ++ errmsg("too many array dimensions")); ++ attTup->attndims = list_length(typeName->arrayBounds); ++ attTup->attlen = tform->typlen; ++ attTup->attbyval = tform->typbyval; ++ attTup->attalign = tform->typalign; ++ attTup->attstorage = tform->typstorage; ++ attTup->attcompression = InvalidCompressionMethod; + - repl_repl[Anum_pg_class_reloptions - 1] = true; ++ ReleaseSysCache(typeTuple); + - newtuple = heap_modify_tuple(tuple, RelationGetDescr(pgclass), - repl_val, repl_null, repl_repl); ++ CatalogTupleUpdate(attrelation, &heapTup->t_self, heapTup); + - CatalogTupleUpdate(pgclass, &newtuple->t_self, newtuple); - UnlockTuple(pgclass, &tuple->t_self, InplaceUpdateTupleLock); ++ table_close(attrelation, RowExclusiveLock); + - InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0); ++ /* Install dependencies on new datatype and collation */ ++ add_column_datatype_dependency(RelationGetRelid(rel), attnum, targettype); ++ add_column_collation_dependency(RelationGetRelid(rel), attnum, targetcollid); + - heap_freetuple(newtuple); ++ /* ++ * Drop any pg_statistic entry for the column, since it's now wrong type ++ */ ++ RemoveStatistics(RelationGetRelid(rel), attnum); + - ReleaseSysCache(tuple); ++ InvokeObjectPostAlterHook(RelationRelationId, ++ RelationGetRelid(rel), attnum); + - /* repeat the whole exercise for the toast table, if there's one */ - if (OidIsValid(rel->rd_rel->reltoastrelid)) ++ /* ++ * Update the default, if present, by brute force --- remove and re-add ++ * the default. Probably unsafe to take shortcuts, since the new version ++ * may well have additional dependencies. (It's okay to do this now, ++ * rather than after other ALTER TYPE commands, since the default won't ++ * depend on other column types.) ++ */ ++ if (defaultexpr) + { - Relation toastrel; - Oid toastid = rel->rd_rel->reltoastrelid; - - toastrel = table_open(toastid, lockmode); - - /* Fetch heap tuple */ - tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(toastid)); - if (!HeapTupleIsValid(tuple)) - elog(ERROR, "cache lookup failed for relation %u", toastid); - - if (operation == AT_ReplaceRelOptions) - { - /* - * If we're supposed to replace the reloptions list, we just - * pretend there were none before. - */ - datum = (Datum) 0; - } - else ++ /* ++ * If it's a GENERATED default, drop its dependency records, in ++ * particular its INTERNAL dependency on the column, which would ++ * otherwise cause dependency.c to refuse to perform the deletion. ++ */ ++ if (attTup->attgenerated) + { - bool isnull; ++ Oid attrdefoid = GetAttrDefaultOid(RelationGetRelid(rel), attnum); + - /* Get the old reloptions */ - datum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, - &isnull); - if (isnull) - datum = (Datum) 0; ++ if (!OidIsValid(attrdefoid)) ++ elog(ERROR, "could not find attrdef tuple for relation %u attnum %d", ++ RelationGetRelid(rel), attnum); ++ (void) deleteDependencyRecordsFor(AttrDefaultRelationId, attrdefoid, false); + } + - newOptions = transformRelOptions(datum, defList, "toast", validnsps, - false, operation == AT_ResetRelOptions); - - (void) heap_reloptions(RELKIND_TOASTVALUE, newOptions, true); - - memset(repl_val, 0, sizeof(repl_val)); - memset(repl_null, false, sizeof(repl_null)); - memset(repl_repl, false, sizeof(repl_repl)); - - if (newOptions != (Datum) 0) - repl_val[Anum_pg_class_reloptions - 1] = newOptions; - else - repl_null[Anum_pg_class_reloptions - 1] = true; - - repl_repl[Anum_pg_class_reloptions - 1] = true; - - newtuple = heap_modify_tuple(tuple, RelationGetDescr(pgclass), - repl_val, repl_null, repl_repl); - - CatalogTupleUpdate(pgclass, &newtuple->t_self, newtuple); ++ /* ++ * Make updates-so-far visible, particularly the new pg_attribute row ++ * which will be updated again. ++ */ ++ CommandCounterIncrement(); + - InvokeObjectPostAlterHookArg(RelationRelationId, - RelationGetRelid(toastrel), 0, - InvalidOid, true); ++ /* ++ * We use RESTRICT here for safety, but at present we do not expect ++ * anything to depend on the default. ++ */ ++ RemoveAttrDefault(RelationGetRelid(rel), attnum, DROP_RESTRICT, true, ++ true); + - heap_freetuple(newtuple); ++ (void) StoreAttrDefault(rel, attnum, defaultexpr, true); ++ } + - ReleaseSysCache(tuple); ++ ObjectAddressSubSet(address, RelationRelationId, ++ RelationGetRelid(rel), attnum); + - table_close(toastrel, NoLock); - } ++ /* Cleanup */ ++ heap_freetuple(heapTup); + - table_close(pgclass, RowExclusiveLock); ++ return address; +} + +/* - * 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. ++ * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything ++ * that depends on the column (constraints, indexes, etc), and record enough ++ * information to let us recreate the objects. + */ +static void - ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode) ++RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, ++ Relation rel, AttrNumber attnum, const char *colName) +{ - Relation rel; - Oid reltoastrelid; - RelFileNumber newrelfilenumber; - RelFileLocator newrlocator; - List *reltoastidxids = NIL; - ListCell *lc; ++ Relation depRel; ++ ScanKeyData key[3]; ++ SysScanDesc scan; ++ HeapTuple depTup; + - /* - * Need lock here in case we are recursing to toast table or index - */ - rel = relation_open(tableOid, lockmode); ++ Assert(subtype == AT_AlterColumnType || subtype == AT_SetExpression); + - /* Check first if relation can be moved to new tablespace */ - if (!CheckRelationTableSpaceMove(rel, newTableSpace)) - { - InvokeObjectPostAlterHook(RelationRelationId, - RelationGetRelid(rel), 0); - relation_close(rel, NoLock); - return; - } ++ depRel = table_open(DependRelationId, RowExclusiveLock); + - reltoastrelid = rel->rd_rel->reltoastrelid; - /* Fetch the list of indexes on toast relation if necessary */ - if (OidIsValid(reltoastrelid)) - { - Relation toastRel = relation_open(reltoastrelid, lockmode); ++ ScanKeyInit(&key[0], ++ Anum_pg_depend_refclassid, ++ BTEqualStrategyNumber, F_OIDEQ, ++ ObjectIdGetDatum(RelationRelationId)); ++ ScanKeyInit(&key[1], ++ Anum_pg_depend_refobjid, ++ BTEqualStrategyNumber, F_OIDEQ, ++ ObjectIdGetDatum(RelationGetRelid(rel))); ++ ScanKeyInit(&key[2], ++ Anum_pg_depend_refobjsubid, ++ BTEqualStrategyNumber, F_INT4EQ, ++ Int32GetDatum((int32) attnum)); + - reltoastidxids = RelationGetIndexList(toastRel); - relation_close(toastRel, lockmode); ++ scan = systable_beginscan(depRel, DependReferenceIndexId, true, ++ NULL, 3, key); ++ ++ while (HeapTupleIsValid(depTup = systable_getnext(scan))) ++ { ++ Form_pg_depend foundDep = (Form_pg_depend) GETSTRUCT(depTup); ++ ObjectAddress foundObject; ++ ++ foundObject.classId = foundDep->classid; ++ foundObject.objectId = foundDep->objid; ++ foundObject.objectSubId = foundDep->objsubid; ++ ++ switch (foundObject.classId) ++ { ++ case RelationRelationId: ++ { ++ char relKind = get_rel_relkind(foundObject.objectId); ++ ++ if (relKind == RELKIND_INDEX || ++ relKind == RELKIND_PARTITIONED_INDEX) ++ { ++ Assert(foundObject.objectSubId == 0); ++ RememberIndexForRebuilding(foundObject.objectId, tab); ++ } ++ else if (relKind == RELKIND_SEQUENCE) ++ { ++ /* ++ * This must be a SERIAL column's sequence. We need ++ * not do anything to it. ++ */ ++ Assert(foundObject.objectSubId == 0); ++ } ++ else ++ { ++ /* Not expecting any other direct dependencies... */ ++ elog(ERROR, "unexpected object depending on column: %s", ++ getObjectDescription(&foundObject, false)); ++ } ++ break; ++ } ++ ++ case ConstraintRelationId: ++ Assert(foundObject.objectSubId == 0); ++ RememberConstraintForRebuilding(foundObject.objectId, tab); ++ break; ++ ++ case ProcedureRelationId: ++ ++ /* ++ * A new-style SQL function can depend on a column, if that ++ * column is referenced in the parsed function body. Ideally ++ * we'd automatically update the function by deparsing and ++ * reparsing it, but that's risky and might well fail anyhow. ++ * FIXME someday. ++ * ++ * This is only a problem for AT_AlterColumnType, not ++ * AT_SetExpression. ++ */ ++ if (subtype == AT_AlterColumnType) ++ ereport(ERROR, ++ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ++ errmsg("cannot alter type of a column used by a function or procedure"), ++ errdetail("%s depends on column \"%s\"", ++ getObjectDescription(&foundObject, false), ++ colName))); ++ break; ++ ++ case RewriteRelationId: ++ ++ /* ++ * View/rule bodies have pretty much the same issues as ++ * function bodies. FIXME someday. ++ */ ++ if (subtype == AT_AlterColumnType) ++ ereport(ERROR, ++ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ++ errmsg("cannot alter type of a column used by a view or rule"), ++ errdetail("%s depends on column \"%s\"", ++ getObjectDescription(&foundObject, false), ++ colName))); ++ break; ++ ++ case TriggerRelationId: ++ ++ /* ++ * A trigger can depend on a column because the column is ++ * specified as an update target, or because the column is ++ * used in the trigger's WHEN condition. The first case would ++ * not require any extra work, but the second case would ++ * require updating the WHEN expression, which has the same ++ * issues as above. Since we can't easily tell which case ++ * applies, we punt for both. FIXME someday. ++ */ ++ if (subtype == AT_AlterColumnType) ++ ereport(ERROR, ++ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ++ errmsg("cannot alter type of a column used in a trigger definition"), ++ errdetail("%s depends on column \"%s\"", ++ getObjectDescription(&foundObject, false), ++ colName))); ++ break; ++ ++ case PolicyRelationId: ++ ++ /* ++ * A policy can depend on a column because the column is ++ * specified in the policy's USING or WITH CHECK qual ++ * expressions. It might be possible to rewrite and recheck ++ * the policy expression, but punt for now. It's certainly ++ * easy enough to remove and recreate the policy; still, FIXME ++ * someday. ++ */ ++ if (subtype == AT_AlterColumnType) ++ ereport(ERROR, ++ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ++ errmsg("cannot alter type of a column used in a policy definition"), ++ errdetail("%s depends on column \"%s\"", ++ getObjectDescription(&foundObject, false), ++ colName))); ++ break; ++ ++ case AttrDefaultRelationId: ++ { ++ ObjectAddress col = GetAttrDefaultColumnAddress(foundObject.objectId); ++ ++ if (col.objectId == RelationGetRelid(rel) && ++ col.objectSubId == attnum) ++ { ++ /* ++ * Ignore the column's own default expression. The ++ * caller deals with it. ++ */ ++ } ++ else ++ { ++ /* ++ * This must be a reference from the expression of a ++ * generated column elsewhere in the same table. ++ * Changing the type/generated expression of a column ++ * that is used by a generated column is not allowed ++ * by SQL standard, so just punt for now. It might be ++ * doable with some thinking and effort. ++ */ ++ if (subtype == AT_AlterColumnType) ++ ereport(ERROR, ++ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ++ errmsg("cannot alter type of a column used by a generated column"), ++ errdetail("Column \"%s\" is used by generated column \"%s\".", ++ colName, ++ get_attname(col.objectId, ++ col.objectSubId, ++ false)))); ++ } ++ break; ++ } ++ ++ case StatisticExtRelationId: ++ ++ /* ++ * Give the extended-stats machinery a chance to fix anything ++ * that this column type change would break. ++ */ ++ RememberStatisticsForRebuilding(foundObject.objectId, tab); ++ break; ++ ++ case PublicationRelRelationId: ++ ++ /* ++ * Column reference in a PUBLICATION ... FOR TABLE ... WHERE ++ * clause. Same issues as above. FIXME someday. ++ */ ++ if (subtype == AT_AlterColumnType) ++ ereport(ERROR, ++ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ++ errmsg("cannot alter type of a column used by a publication WHERE clause"), ++ errdetail("%s depends on column \"%s\"", ++ getObjectDescription(&foundObject, false), ++ colName))); ++ break; ++ ++ default: ++ ++ /* ++ * We don't expect any other sorts of objects to depend on a ++ * column. ++ */ ++ elog(ERROR, "unexpected object depending on column: %s", ++ getObjectDescription(&foundObject, false)); ++ break; ++ } + } + ++ systable_endscan(scan); ++ table_close(depRel, NoLock); ++} ++ ++/* ++ * Subroutine for ATExecAlterColumnType: remember that a replica identity ++ * needs to be reset. ++ */ ++static void ++RememberReplicaIdentityForRebuilding(Oid indoid, AlteredTableInfo *tab) ++{ ++ if (!get_index_isreplident(indoid)) ++ return; ++ ++ if (tab->replicaIdentityIndex) ++ elog(ERROR, "relation %u has multiple indexes marked as replica identity", tab->relid); ++ ++ tab->replicaIdentityIndex = get_rel_name(indoid); ++} ++ ++/* ++ * Subroutine for ATExecAlterColumnType: remember any clustered index. ++ */ ++static void ++RememberClusterOnForRebuilding(Oid indoid, AlteredTableInfo *tab) ++{ ++ if (!get_index_isclustered(indoid)) ++ return; ++ ++ if (tab->clusterOnIndex) ++ elog(ERROR, "relation %u has multiple clustered indexes", tab->relid); ++ ++ tab->clusterOnIndex = get_rel_name(indoid); ++} ++ ++/* ++ * Subroutine for ATExecAlterColumnType: remember that a constraint needs ++ * to be rebuilt (which we might already know). ++ */ ++static void ++RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab) ++{ ++ /* ++ * This de-duplication check is critical for two independent reasons: we ++ * mustn't try to recreate the same constraint twice, and if a constraint ++ * depends on more than one column whose type is to be altered, we must ++ * capture its definition string before applying any of the column type ++ * changes. ruleutils.c will get confused if we ask again later. ++ */ ++ if (!list_member_oid(tab->changedConstraintOids, conoid)) ++ { ++ /* OK, capture the constraint's existing definition string */ ++ char *defstring = pg_get_constraintdef_command(conoid); ++ Oid indoid; ++ ++ /* ++ * It is critical to create not-null constraints ahead of primary key ++ * indexes; otherwise, the not-null constraint would be created by the ++ * primary key, and the constraint name would be wrong. ++ */ ++ if (get_constraint_type(conoid) == CONSTRAINT_NOTNULL) ++ { ++ tab->changedConstraintOids = lcons_oid(conoid, ++ tab->changedConstraintOids); ++ tab->changedConstraintDefs = lcons(defstring, ++ tab->changedConstraintDefs); ++ } ++ else ++ { ++ ++ tab->changedConstraintOids = lappend_oid(tab->changedConstraintOids, ++ conoid); ++ tab->changedConstraintDefs = lappend(tab->changedConstraintDefs, ++ defstring); ++ } ++ ++ /* ++ * For the index of a constraint, if any, remember if it is used for ++ * the table's replica identity or if it is a clustered index, so that ++ * ATPostAlterTypeCleanup() can queue up commands necessary to restore ++ * those properties. ++ */ ++ indoid = get_constraint_index(conoid); ++ if (OidIsValid(indoid)) ++ { ++ RememberReplicaIdentityForRebuilding(indoid, tab); ++ RememberClusterOnForRebuilding(indoid, tab); ++ } ++ } ++} ++ ++/* ++ * Subroutine for ATExecAlterColumnType: remember that an index needs ++ * to be rebuilt (which we might already know). ++ */ ++static void ++RememberIndexForRebuilding(Oid indoid, AlteredTableInfo *tab) ++{ ++ /* ++ * This de-duplication check is critical for two independent reasons: we ++ * mustn't try to recreate the same index twice, and if an index depends ++ * on more than one column whose type is to be altered, we must capture ++ * its definition string before applying any of the column type changes. ++ * ruleutils.c will get confused if we ask again later. ++ */ ++ if (!list_member_oid(tab->changedIndexOids, indoid)) ++ { ++ /* ++ * Before adding it as an index-to-rebuild, we'd better see if it ++ * belongs to a constraint, and if so rebuild the constraint instead. ++ * Typically this check fails, because constraint indexes normally ++ * have only dependencies on their constraint. But it's possible for ++ * such an index to also have direct dependencies on table columns, ++ * for example with a partial exclusion constraint. ++ */ ++ Oid conoid = get_index_constraint(indoid); + - /* lookup the attribute so we can check inheritance status */ - tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName); - if (!HeapTupleIsValid(tuple)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("column \"%s\" of relation \"%s\" does not exist", - colName, RelationGetRelationName(rel)), - parser_errposition(pstate, def->location))); - attTup = (Form_pg_attribute) GETSTRUCT(tuple); - attnum = attTup->attnum; ++ if (OidIsValid(conoid)) ++ { ++ RememberConstraintForRebuilding(conoid, tab); ++ } ++ else ++ { ++ /* OK, capture the index's existing definition string */ ++ char *defstring = pg_get_indexdef_string(indoid); + - /* Can't alter a system attribute */ - if (attnum <= 0) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot alter system column \"%s\"", colName), - parser_errposition(pstate, def->location))); ++ tab->changedIndexOids = lappend_oid(tab->changedIndexOids, ++ indoid); ++ tab->changedIndexDefs = lappend(tab->changedIndexDefs, ++ defstring); ++ ++ /* ++ * Remember if this index is used for the table's replica identity ++ * or if it is a clustered index, so that ATPostAlterTypeCleanup() ++ * can queue up commands necessary to restore those properties. ++ */ ++ RememberReplicaIdentityForRebuilding(indoid, tab); ++ RememberClusterOnForRebuilding(indoid, tab); ++ } ++ } ++} + ++/* ++ * Subroutine for ATExecAlterColumnType: remember that a statistics object ++ * needs to be rebuilt (which we might already know). ++ */ ++static void ++RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab) ++{ + /* - * Cannot specify USING when altering type of a generated column, because - * that would violate the generation expression. ++ * This de-duplication check is critical for two independent reasons: we ++ * mustn't try to recreate the same statistics object twice, and if the ++ * statistics object depends on more than one column whose type is to be ++ * altered, we must capture its definition string before applying any of ++ * the type changes. ruleutils.c will get confused if we ask again later. + */ - if (attTup->attgenerated && def->cooked_default) - ereport(ERROR, - (errcode(ERRCODE_INVALID_COLUMN_DEFINITION), - errmsg("cannot specify USING when altering type of generated column"), - errdetail("Column \"%s\" is a generated column.", colName), - parser_errposition(pstate, def->location))); ++ if (!list_member_oid(tab->changedStatisticsOids, stxoid)) ++ { ++ /* OK, capture the statistics object's existing definition string */ ++ char *defstring = pg_get_statisticsobjdef_string(stxoid); ++ ++ tab->changedStatisticsOids = lappend_oid(tab->changedStatisticsOids, ++ stxoid); ++ tab->changedStatisticsDefs = lappend(tab->changedStatisticsDefs, ++ defstring); ++ } ++} ++ ++/* ++ * Cleanup after we've finished all the ALTER TYPE or SET EXPRESSION ++ * operations for a particular relation. We have to drop and recreate all the ++ * indexes and constraints that depend on the altered columns. We do the ++ * actual dropping here, but re-creation is managed by adding work queue ++ * entries to do those steps later. ++ */ ++static void ++ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) ++{ ++ ObjectAddress obj; ++ ObjectAddresses *objects; ++ ListCell *def_item; ++ ListCell *oid_item; + /* - * Relfilenumbers are not unique in databases across tablespaces, so we - * need to allocate a new one in the new tablespace. - * Don't alter inherited columns. At outer level, there had better not be - * any inherited definition; when recursing, we assume this was checked at - * the parent level (see below). ++ * Collect all the constraints and indexes to drop so we can process them ++ * in a single call. That way we don't have to worry about dependencies ++ * among them. */ - newrelfilenumber = GetNewRelFileNumber(newTableSpace, NULL, - rel->rd_rel->relpersistence); - - /* Open old and new relation */ - newrlocator = rel->rd_locator; - newrlocator.relNumber = newrelfilenumber; - newrlocator.spcOid = newTableSpace; - - /* hand off to AM to actually create new rel storage and copy the data */ - if (rel->rd_rel->relkind == RELKIND_INDEX) - { - index_copy_data(rel, newrlocator); - } - else - { - Assert(RELKIND_HAS_TABLE_AM(rel->rd_rel->relkind)); - table_relation_copy_data(rel, &newrlocator); - } - if (attTup->attinhcount > 0 && !recursing) - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("cannot alter inherited column \"%s\"", colName), - parser_errposition(pstate, def->location))); ++ objects = new_object_addresses(); - /* Don't alter columns used in the partition key */ - if (has_partition_attrs(rel, - bms_make_singleton(attnum - FirstLowInvalidHeapAttributeNumber), - &is_expr)) - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("cannot alter column \"%s\" because it is part of the partition key of relation \"%s\"", - colName, RelationGetRelationName(rel)), - parser_errposition(pstate, def->location))); + /* - * Update the pg_class row. ++ * Re-parse the index and constraint definitions, and attach them to the ++ * appropriate work queue entries. We do this before dropping because in ++ * the case of a constraint on another table, we might not yet have ++ * exclusive lock on the table the constraint is attached to, and we need ++ * to get that before reparsing/dropping. (That's possible at least for ++ * FOREIGN KEY, CHECK, and EXCLUSION constraints; in non-FK cases it ++ * requires a dependency on the target table's composite type in the other ++ * table's constraint expressions.) + * - * 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(). ++ * We can't rely on the output of deparsing to tell us which relation to ++ * operate on, because concurrent activity might have made the name ++ * resolve differently. Instead, we've got to use the OID of the ++ * constraint or index we're processing to figure out which relation to ++ * operate on. + */ - SetRelationTableSpace(rel, newTableSpace, newrelfilenumber); ++ forboth(oid_item, tab->changedConstraintOids, ++ def_item, tab->changedConstraintDefs) ++ { ++ Oid oldId = lfirst_oid(oid_item); ++ HeapTuple tup; ++ Form_pg_constraint con; ++ Oid relid; ++ Oid confrelid; ++ bool conislocal; - InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0); - /* Look up the target type */ - typenameTypeIdAndMod(pstate, typeName, &targettype, &targettypmod); ++ tup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(oldId)); ++ if (!HeapTupleIsValid(tup)) /* should not happen */ ++ elog(ERROR, "cache lookup failed for constraint %u", oldId); ++ con = (Form_pg_constraint) GETSTRUCT(tup); ++ if (OidIsValid(con->conrelid)) ++ relid = con->conrelid; ++ else ++ { ++ /* must be a domain constraint */ ++ relid = get_typ_typrelid(getBaseType(con->contypid)); ++ if (!OidIsValid(relid)) ++ elog(ERROR, "could not identify relation associated with constraint %u", oldId); ++ } ++ confrelid = con->confrelid; ++ conislocal = con->conislocal; ++ ReleaseSysCache(tup); - RelationAssumeNewRelfilelocator(rel); - aclresult = object_aclcheck(TypeRelationId, targettype, GetUserId(), ACL_USAGE); - if (aclresult != ACLCHECK_OK) - aclcheck_error_type(aclresult, targettype); ++ ObjectAddressSet(obj, ConstraintRelationId, oldId); ++ add_exact_object_address(&obj, objects); - relation_close(rel, NoLock); - /* And the collation */ - targetcollid = GetColumnDefCollation(pstate, def, targettype); ++ /* ++ * If the constraint is inherited (only), we don't want to inject a ++ * new definition here; it'll get recreated when ++ * ATAddCheckNNConstraint recurses from adding the parent table's ++ * constraint. But we had to carry the info this far so that we can ++ * drop the constraint below. ++ */ ++ if (!conislocal) ++ continue; - /* Make sure the reltablespace change is visible */ - CommandCounterIncrement(); - /* make sure datatype is legal for a column */ - CheckAttributeType(colName, targettype, targetcollid, - list_make1_oid(rel->rd_rel->reltype), - (attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL ? CHKATYPE_IS_VIRTUAL : 0)); ++ /* ++ * When rebuilding another table's constraint that references the ++ * table we're modifying, we might not yet have any lock on the other ++ * table, so get one now. We'll need AccessExclusiveLock for the DROP ++ * CONSTRAINT step, so there's no value in asking for anything weaker. ++ */ ++ if (relid != tab->relid) ++ LockRelationOid(relid, AccessExclusiveLock); - /* Move associated toast relation and/or indexes, too */ - if (OidIsValid(reltoastrelid)) - ATExecSetTableSpace(reltoastrelid, newTableSpace, lockmode); - foreach(lc, reltoastidxids) - ATExecSetTableSpace(lfirst_oid(lc), newTableSpace, lockmode); - if (attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) - { - /* do nothing */ ++ ATPostAlterTypeParse(oldId, relid, confrelid, ++ (char *) lfirst(def_item), ++ wqueue, lockmode, tab->rewrite); + } - else if (tab->relkind == RELKIND_RELATION || - tab->relkind == RELKIND_PARTITIONED_TABLE) ++ forboth(oid_item, tab->changedIndexOids, ++ def_item, tab->changedIndexDefs) + { ++ Oid oldId = lfirst_oid(oid_item); ++ Oid relid; + - /* Clean up */ - list_free(reltoastidxids); - } ++ relid = IndexGetRelation(oldId, false); + - /* - * Special handling of ALTER TABLE SET TABLESPACE for relations with no - * storage that have an interest in preserving tablespace. - * - * Since these have no storage the tablespace can be updated with a simple - * metadata only operation to update the tablespace. - */ - static void - ATExecSetTableSpaceNoStorage(Relation rel, Oid newTableSpace) - { - /* - * Shouldn't be called on relations having storage; these are processed in - * phase 3. - */ - Assert(!RELKIND_HAS_STORAGE(rel->rd_rel->relkind)); + /* - * Set up an expression to transform the old data value to the new - * type. If a USING option was given, use the expression as - * transformed by transformAlterTableStmt, else just take the old - * value and try to coerce it. We do this first so that type - * incompatibility can be detected before we waste effort, and because - * we need the expression to be parsed against the original table row - * type. ++ * As above, make sure we have lock on the index's table if it's not ++ * the same table. + */ - if (!transform) - { - transform = (Node *) makeVar(1, attnum, - attTup->atttypid, attTup->atttypmod, - attTup->attcollation, - 0); - } ++ if (relid != tab->relid) ++ LockRelationOid(relid, AccessExclusiveLock); - /* check if relation can be moved to its new tablespace */ - if (!CheckRelationTableSpaceMove(rel, newTableSpace)) - { - InvokeObjectPostAlterHook(RelationRelationId, - RelationGetRelid(rel), - 0); - return; - transform = coerce_to_target_type(pstate, - transform, exprType(transform), - targettype, targettypmod, - COERCION_ASSIGNMENT, - COERCE_IMPLICIT_CAST, - -1); - if (transform == NULL) - { - /* error text depends on whether USING was specified or not */ - if (def->cooked_default != NULL) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("result of USING clause for column \"%s\"" - " cannot be cast automatically to type %s", - colName, format_type_be(targettype)), - errhint("You might need to add an explicit cast."))); - else - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("column \"%s\" cannot be cast automatically to type %s", - colName, format_type_be(targettype)), - !attTup->attgenerated ? - /* translator: USING is SQL, don't translate it */ - errhint("You might need to specify \"USING %s::%s\".", - quote_identifier(colName), - format_type_with_typemod(targettype, - targettypmod)) : 0)); - } ++ ATPostAlterTypeParse(oldId, relid, InvalidOid, ++ (char *) lfirst(def_item), ++ wqueue, lockmode, tab->rewrite); + - /* Fix collations after all else */ - assign_expr_collations(pstate, transform); ++ ObjectAddressSet(obj, RelationRelationId, oldId); ++ add_exact_object_address(&obj, objects); + } - /* Update can be done, so change reltablespace */ - SetRelationTableSpace(rel, newTableSpace, InvalidOid); - /* Expand virtual generated columns in the expr. */ - transform = expand_generated_columns_in_expr(transform, rel, 1); ++ /* add dependencies for new statistics */ ++ forboth(oid_item, tab->changedStatisticsOids, ++ def_item, tab->changedStatisticsDefs) ++ { ++ Oid oldId = lfirst_oid(oid_item); ++ Oid relid; - InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0); - /* Plan the expr now so we can accurately assess the need to rewrite. */ - transform = (Node *) expression_planner((Expr *) transform); ++ relid = StatisticsGetRelation(oldId, false); - /* Make sure the reltablespace change is visible */ - CommandCounterIncrement(); - } + /* - * Add a work queue item to make ATRewriteTable update the column - * contents. ++ * As above, make sure we have lock on the statistics object's table ++ * if it's not the same table. However, we take ++ * ShareUpdateExclusiveLock here, aligning with the lock level used in ++ * CreateStatistics and RemoveStatisticsById. ++ * ++ * CAUTION: this should be done after all cases that grab ++ * AccessExclusiveLock, else we risk causing deadlock due to needing ++ * to promote our table lock. + */ - newval = (NewColumnValue *) palloc0(sizeof(NewColumnValue)); - newval->attnum = attnum; - newval->expr = (Expr *) transform; - newval->is_generated = false; ++ if (relid != tab->relid) ++ LockRelationOid(relid, ShareUpdateExclusiveLock); - /* - * 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); - tab->newvals = lappend(tab->newvals, newval); - if (ATColumnChangeRequiresRewrite(transform, attnum)) - tab->rewrite |= AT_REWRITE_COLUMN_REWRITE; ++ ATPostAlterTypeParse(oldId, relid, InvalidOid, ++ (char *) lfirst(def_item), ++ wqueue, lockmode, tab->rewrite); + - /* 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"))); ++ ObjectAddressSet(obj, StatisticExtRelationId, oldId); ++ add_exact_object_address(&obj, objects); + } - else if (transform) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is not a table", - RelationGetRelationName(rel)))); - /* 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); - if (!RELKIND_HAS_STORAGE(tab->relkind) || attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) ++ /* ++ * Queue up command to restore replica identity index marking ++ */ ++ if (tab->replicaIdentityIndex) + { - /* - * For relations or columns without storage, do this check now. - * Regular tables will check it later when the table is being - * rewritten. - */ - find_composite_type_dependencies(rel->rd_rel->reltype, rel, NULL); ++ AlterTableCmd *cmd = makeNode(AlterTableCmd); ++ ReplicaIdentityStmt *subcmd = makeNode(ReplicaIdentityStmt); + - /* 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"))); ++ subcmd->identity_type = REPLICA_IDENTITY_INDEX; ++ subcmd->name = tab->replicaIdentityIndex; ++ cmd->subtype = AT_ReplicaIdentity; ++ cmd->def = (Node *) subcmd; ++ ++ /* do it after indexes and constraints */ ++ tab->subcmds[AT_PASS_OLD_CONSTR] = ++ lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd); + } + - ReleaseSysCache(tuple); ++ /* ++ * Queue up command to restore marking of index used for cluster. ++ */ ++ if (tab->clusterOnIndex) ++ { ++ AlterTableCmd *cmd = makeNode(AlterTableCmd); ++ ++ cmd->subtype = AT_ClusterOn; ++ cmd->name = tab->clusterOnIndex; ++ ++ /* do it after indexes and constraints */ ++ tab->subcmds[AT_PASS_OLD_CONSTR] = ++ lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd); ++ } /* - * Must have CREATE rights on the new tablespace, unless it is the - * database default tablespace (which all users implicitly have CREATE - * rights on). - * Recurse manually by queueing a new command for each child, if - * necessary. We cannot apply ATSimpleRecursion here because we need to - * remap attribute numbers in the USING expression, if any. ++ * It should be okay to use DROP_RESTRICT here, since nothing else should ++ * be depending on these objects. + */ - if (OidIsValid(new_tablespaceoid) && new_tablespaceoid != MyDatabaseTableSpace) - { - AclResult aclresult; ++ performMultipleDeletions(objects, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); + - aclresult = object_aclcheck(TableSpaceRelationId, new_tablespaceoid, GetUserId(), - ACL_CREATE); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_TABLESPACE, - get_tablespace_name(new_tablespaceoid)); - } ++ free_object_addresses(objects); + + /* - * Now that the checks are done, check if we should set either to - * InvalidOid because it is our database's default tablespace. ++ * The objects will get recreated during subsequent passes over the work ++ * queue. + */ - if (orig_tablespaceoid == MyDatabaseTableSpace) - orig_tablespaceoid = InvalidOid; - - if (new_tablespaceoid == MyDatabaseTableSpace) - new_tablespaceoid = InvalidOid; ++} + - /* no-op */ - if (orig_tablespaceoid == new_tablespaceoid) - return new_tablespaceoid; ++/* ++ * Parse the previously-saved definition string for a constraint, index or ++ * statistics object against the newly-established column data type(s), and ++ * queue up the resulting command parsetrees for execution. ++ * ++ * This might fail if, for example, you have a WHERE clause that uses an ++ * operator that's not available for the new column type. ++ */ ++static void ++ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd, ++ List **wqueue, LOCKMODE lockmode, bool rewrite) ++{ ++ List *raw_parsetree_list; ++ List *querytree_list; ++ ListCell *list_item; ++ Relation rel; + + /* - * Walk the list of objects in the tablespace and move them. This will - * only find objects in our database, of course. ++ * We expect that we will get only ALTER TABLE and CREATE INDEX ++ * statements. Hence, there is no need to pass them through ++ * parse_analyze_*() or the rewriter, but instead we need to pass them ++ * through parse_utilcmd.c to make them ready for execution. + */ - ScanKeyInit(&key[0], - Anum_pg_class_reltablespace, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(orig_tablespaceoid)); - - rel = table_open(RelationRelationId, AccessShareLock); - scan = table_beginscan_catalog(rel, 1, key); - while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) ++ raw_parsetree_list = raw_parser(cmd, RAW_PARSE_DEFAULT); ++ querytree_list = NIL; ++ foreach(list_item, raw_parsetree_list) + { - Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple); - Oid relOid = relForm->oid; ++ RawStmt *rs = lfirst_node(RawStmt, list_item); ++ Node *stmt = rs->stmt; + - /* - * 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 (IsCatalogNamespace(relForm->relnamespace) || - relForm->relisshared || - isAnyTempNamespace(relForm->relnamespace) || - IsToastNamespace(relForm->relnamespace)) - continue; ++ if (IsA(stmt, IndexStmt)) ++ querytree_list = lappend(querytree_list, ++ transformIndexStmt(oldRelId, ++ (IndexStmt *) stmt, ++ cmd)); ++ else if (IsA(stmt, AlterTableStmt)) ++ { ++ List *beforeStmts; ++ List *afterStmts; + - /* 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; ++ stmt = (Node *) transformAlterTableStmt(oldRelId, ++ (AlterTableStmt *) stmt, ++ cmd, ++ &beforeStmts, ++ &afterStmts); ++ querytree_list = list_concat(querytree_list, beforeStmts); ++ querytree_list = lappend(querytree_list, stmt); ++ querytree_list = list_concat(querytree_list, afterStmts); ++ } ++ else if (IsA(stmt, CreateStatsStmt)) ++ querytree_list = lappend(querytree_list, ++ transformStatsStmt(oldRelId, ++ (CreateStatsStmt *) stmt, ++ cmd)); ++ else ++ querytree_list = lappend(querytree_list, stmt); ++ } + - /* Check if we are only moving objects owned by certain roles */ - if (role_oids != NIL && !list_member_oid(role_oids, relForm->relowner)) - continue; ++ /* Caller should already have acquired whatever lock we need. */ ++ rel = relation_open(oldRelId, NoLock); + - /* - * Handle permissions-checking here since we are locking the tables - * and also to avoid doing a bunch of work only to fail part-way. Note - * that permissions will also be checked by AlterTableInternal(). - * - * Caller must be considered an owner on the table to move it. - */ - if (!object_ownercheck(RelationRelationId, relOid, GetUserId())) - aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relOid)), - NameStr(relForm->relname)); ++ /* ++ * Attach each generated command to the proper place in the work queue. ++ * Note this could result in creation of entirely new work-queue entries. + * - * If we are told not to recurse, there had better not be any child - * tables; else the alter would put them out of step. ++ * Also note that we have to tweak the command subtypes, because it turns ++ * out that re-creation of indexes and constraints has to act a bit ++ * differently from initial creation. + */ - if (recurse) ++ foreach(list_item, querytree_list) + { - Oid relid = RelationGetRelid(rel); - List *child_oids, - *child_numparents; - ListCell *lo, - *li; ++ Node *stm = (Node *) lfirst(list_item); ++ AlteredTableInfo *tab; - if (stmt->nowait && - !ConditionalLockRelationOid(relOid, AccessExclusiveLock)) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_IN_USE), - errmsg("aborting because lock on relation \"%s.%s\" is not available", - get_namespace_name(relForm->relnamespace), - NameStr(relForm->relname)))); - else - LockRelationOid(relOid, AccessExclusiveLock); - child_oids = find_all_inheritors(relid, lockmode, - &child_numparents); ++ tab = ATGetQueueEntry(wqueue, rel); - /* Add to our list of objects to move */ - relations = lappend_oid(relations, relOid); - } - /* - * find_all_inheritors does the recursive search of the inheritance - * hierarchy, so all we have to do is process all of the relids in the - * list that it returns. - */ - forboth(lo, child_oids, li, child_numparents) ++ if (IsA(stm, IndexStmt)) + { - Oid childrelid = lfirst_oid(lo); - int numparents = lfirst_int(li); - Relation childrel; - HeapTuple childtuple; - Form_pg_attribute childattTup; ++ IndexStmt *stmt = (IndexStmt *) stm; ++ AlterTableCmd *newcmd; - table_endscan(scan); - table_close(rel, AccessShareLock); - if (childrelid == relid) - continue; ++ if (!rewrite) ++ TryReuseIndex(oldId, stmt); ++ stmt->reset_default_tblspc = true; ++ /* keep the index's comment */ ++ stmt->idxcomment = GetComment(oldId, RelationRelationId, 0); - 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)))); - /* find_all_inheritors already got lock */ - childrel = relation_open(childrelid, NoLock); - CheckAlterTableIsSafe(childrel); ++ newcmd = makeNode(AlterTableCmd); ++ newcmd->subtype = AT_ReAddIndex; ++ newcmd->def = (Node *) stmt; ++ tab->subcmds[AT_PASS_OLD_INDEX] = ++ lappend(tab->subcmds[AT_PASS_OLD_INDEX], newcmd); ++ } ++ else if (IsA(stm, AlterTableStmt)) ++ { ++ AlterTableStmt *stmt = (AlterTableStmt *) stm; ++ ListCell *lcmd; - /* Everything is locked, loop through and move all of the relations. */ - foreach(l, relations) - { - List *cmds = NIL; - AlterTableCmd *cmd = makeNode(AlterTableCmd); - /* - * Verify that the child doesn't have any inherited definitions of - * this column that came from outside this inheritance hierarchy. - * (renameatt makes a similar test, though in a different way - * because of its different recursion mechanism.) - */ - childtuple = SearchSysCacheAttName(RelationGetRelid(childrel), - colName); - if (!HeapTupleIsValid(childtuple)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("column \"%s\" of relation \"%s\" does not exist", - colName, RelationGetRelationName(childrel)))); - childattTup = (Form_pg_attribute) GETSTRUCT(childtuple); ++ foreach(lcmd, stmt->cmds) ++ { ++ AlterTableCmd *cmd = lfirst_node(AlterTableCmd, lcmd); + - cmd->subtype = AT_SetTableSpace; - cmd->name = stmt->new_tablespacename; ++ if (cmd->subtype == AT_AddIndex) ++ { ++ IndexStmt *indstmt; ++ Oid indoid; + - cmds = lappend(cmds, cmd); ++ indstmt = castNode(IndexStmt, cmd->def); ++ indoid = get_constraint_index(oldId); + - EventTriggerAlterTableStart((Node *) stmt); - /* OID is set by AlterTableInternal */ - AlterTableInternal(lfirst_oid(l), cmds, false); - EventTriggerAlterTableEnd(); - } ++ if (!rewrite) ++ TryReuseIndex(indoid, indstmt); ++ /* keep any comment on the index */ ++ indstmt->idxcomment = GetComment(indoid, ++ RelationRelationId, 0); ++ indstmt->reset_default_tblspc = true; + - return new_tablespaceoid; - } ++ cmd->subtype = AT_ReAddIndex; ++ tab->subcmds[AT_PASS_OLD_INDEX] = ++ lappend(tab->subcmds[AT_PASS_OLD_INDEX], cmd); + - static void - index_copy_data(Relation rel, RelFileLocator newrlocator) - { - SMgrRelation dstrel; ++ /* recreate any comment on the constraint */ ++ RebuildConstraintComment(tab, ++ AT_PASS_OLD_INDEX, ++ oldId, ++ rel, ++ NIL, ++ indstmt->idxname); ++ } ++ else if (cmd->subtype == AT_AddConstraint) ++ { ++ Constraint *con = castNode(Constraint, cmd->def); - /* - * Since we copy the file directly without looking at the shared buffers, - * we'd better first flush out any pages of the source relation that are - * in shared buffers. We assume no new changes will be made while we are - * holding exclusive lock on the rel. - */ - FlushRelationBuffers(rel); - if (childattTup->attinhcount > numparents) - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("cannot alter inherited column \"%s\" of relation \"%s\"", - colName, RelationGetRelationName(childrel)))); ++ con->old_pktable_oid = refRelId; ++ /* rewriting neither side of a FK */ ++ if (con->contype == CONSTR_FOREIGN && ++ !rewrite && tab->rewrite == 0) ++ TryReuseForeignKey(oldId, con); ++ con->reset_default_tblspc = true; ++ cmd->subtype = AT_ReAddConstraint; ++ tab->subcmds[AT_PASS_OLD_CONSTR] = ++ lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd); + - ReleaseSysCache(childtuple); ++ /* ++ * Recreate any comment on the constraint. If we have ++ * recreated a primary key, then transformTableConstraint ++ * has added an unnamed not-null constraint here; skip ++ * this in that case. ++ */ ++ if (con->conname) ++ RebuildConstraintComment(tab, ++ AT_PASS_OLD_CONSTR, ++ oldId, ++ rel, ++ NIL, ++ con->conname); ++ else ++ Assert(con->contype == CONSTR_NOTNULL); ++ } ++ else ++ elog(ERROR, "unexpected statement subtype: %d", ++ (int) cmd->subtype); ++ } ++ } ++ else if (IsA(stm, AlterDomainStmt)) ++ { ++ AlterDomainStmt *stmt = (AlterDomainStmt *) stm; - /* - * Create and copy all forks of the relation, and schedule unlinking of - * old physical files. - * - * NOTE: any conflict in relfilenumber value will be caught in - * RelationCreateStorage(). - */ - dstrel = RelationCreateStorage(newrlocator, rel->rd_rel->relpersistence, true); - /* - * Remap the attribute numbers. If no USING expression was - * specified, there is no need for this step. - */ - if (def->cooked_default) ++ if (stmt->subtype == AD_AddConstraint) + { - AttrMap *attmap; - bool found_whole_row; ++ Constraint *con = castNode(Constraint, stmt->def); ++ AlterTableCmd *cmd = makeNode(AlterTableCmd); - /* copy main fork */ - RelationCopyStorage(RelationGetSmgr(rel), dstrel, MAIN_FORKNUM, - rel->rd_rel->relpersistence); - /* create a copy to scribble on */ - cmd = copyObject(cmd); ++ cmd->subtype = AT_ReAddDomainConstraint; ++ cmd->def = (Node *) stmt; ++ tab->subcmds[AT_PASS_OLD_CONSTR] = ++ lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd); - /* copy those extra forks that exist */ - for (ForkNumber forkNum = MAIN_FORKNUM + 1; - forkNum <= MAX_FORKNUM; forkNum++) - { - if (smgrexists(RelationGetSmgr(rel), forkNum)) - attmap = build_attrmap_by_name(RelationGetDescr(childrel), - RelationGetDescr(rel), - false); - ((ColumnDef *) cmd->def)->cooked_default = - map_variable_attnos(def->cooked_default, - 1, 0, - attmap, - InvalidOid, &found_whole_row); - if (found_whole_row) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot convert whole-row table reference"), - errdetail("USING expression contains a whole-row table reference."))); - pfree(attmap); ++ /* recreate any comment on the constraint */ ++ RebuildConstraintComment(tab, ++ AT_PASS_OLD_CONSTR, ++ oldId, ++ NULL, ++ stmt->typeName, ++ con->conname); + } - ATPrepCmd(wqueue, childrel, cmd, false, true, lockmode, context); - relation_close(childrel, NoLock); ++ else ++ elog(ERROR, "unexpected statement subtype: %d", ++ (int) stmt->subtype); ++ } ++ else if (IsA(stm, CreateStatsStmt)) + { - smgrcreate(dstrel, forkNum, false); ++ CreateStatsStmt *stmt = (CreateStatsStmt *) stm; ++ AlterTableCmd *newcmd; + - /* - * WAL log creation if the relation is persistent, or this is the - * init fork of an unlogged relation. - */ - if (RelationIsPermanent(rel) || - (rel->rd_rel->relpersistence == RELPERSISTENCE_UNLOGGED && - forkNum == INIT_FORKNUM)) - log_smgrcreate(&newrlocator, forkNum); - RelationCopyStorage(RelationGetSmgr(rel), dstrel, forkNum, - rel->rd_rel->relpersistence); ++ /* keep the statistics object's comment */ ++ stmt->stxcomment = GetComment(oldId, StatisticExtRelationId, 0); ++ ++ newcmd = makeNode(AlterTableCmd); ++ newcmd->subtype = AT_ReAddStatistics; ++ newcmd->def = (Node *) stmt; ++ tab->subcmds[AT_PASS_MISC] = ++ lappend(tab->subcmds[AT_PASS_MISC], newcmd); } ++ else ++ elog(ERROR, "unexpected statement type: %d", ++ (int) nodeTag(stm)); } - else if (!recursing && - find_inheritance_children(RelationGetRelid(rel), NoLock) != NIL) - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("type of inherited column \"%s\" must be changed in child tables too", - colName))); - /* drop old relation, and close new one */ - RelationDropStorage(rel); - smgrclose(dstrel); - if (tab->relkind == RELKIND_COMPOSITE_TYPE) - ATTypedTableRecursion(wqueue, rel, cmd, lockmode, context); ++ relation_close(rel, NoLock); } /* - * ALTER TABLE ENABLE/DISABLE TRIGGER - * When the data type of a column is changed, a rewrite might not be required - * if the new type is sufficiently identical to the old one, and the USING - * clause isn't trying to insert some other value. It's safe to skip the - * rewrite in these cases: - * - * - the old type is binary coercible to the new type - * - the new type is an unconstrained domain over the old type - * - {NEW,OLD} or {OLD,NEW} is {timestamptz,timestamp} and the timezone is UTC ++ * Subroutine for ATPostAlterTypeParse() to recreate any existing comment ++ * for a table or domain constraint that is being rebuilt. * - * We just pass this off to trigger.c. - * In the case of a constrained domain, we could get by with scanning the - * table and checking the constraint rather than actually rewriting it, but we - * don't currently try to do that. ++ * objid is the OID of the constraint. ++ * Pass "rel" for a table constraint, or "domname" (domain's qualified name ++ * as a string list) for a domain constraint. ++ * (We could dig that info, as well as the conname, out of the pg_constraint ++ * entry; but callers already have them so might as well pass them.) */ -static bool -ATColumnChangeRequiresRewrite(Node *expr, AttrNumber varattno) +static void - ATExecEnableDisableTrigger(Relation rel, const char *trigname, - char fires_when, bool skip_system, bool recurse, - LOCKMODE lockmode) ++RebuildConstraintComment(AlteredTableInfo *tab, AlterTablePass pass, Oid objid, ++ Relation rel, List *domname, ++ const char *conname) { - EnableDisableTrigger(rel, trigname, InvalidOid, - fires_when, skip_system, recurse, - lockmode); - Assert(expr != NULL); ++ CommentStmt *cmd; ++ char *comment_str; ++ AlterTableCmd *newcmd; - InvokeObjectPostAlterHook(RelationRelationId, - RelationGetRelid(rel), 0); - for (;;) ++ /* Look for comment for object wanted, and leave if none */ ++ comment_str = GetComment(objid, ConstraintRelationId, 0); ++ if (comment_str == NULL) ++ return; ++ ++ /* Build CommentStmt node, copying all input data for safety */ ++ cmd = makeNode(CommentStmt); ++ if (rel) + { - /* only one varno, so no need to check that */ - if (IsA(expr, Var) && ((Var *) expr)->varattno == varattno) - return false; - else if (IsA(expr, RelabelType)) - expr = (Node *) ((RelabelType *) expr)->arg; - else if (IsA(expr, CoerceToDomain)) - { - CoerceToDomain *d = (CoerceToDomain *) expr; ++ cmd->objtype = OBJECT_TABCONSTRAINT; ++ cmd->object = (Node *) ++ list_make3(makeString(get_namespace_name(RelationGetNamespace(rel))), ++ makeString(pstrdup(RelationGetRelationName(rel))), ++ makeString(pstrdup(conname))); ++ } ++ else ++ { ++ cmd->objtype = OBJECT_DOMCONSTRAINT; ++ cmd->object = (Node *) ++ list_make2(makeTypeNameFromNameList(copyObject(domname)), ++ makeString(pstrdup(conname))); ++ } ++ cmd->comment = comment_str; + - if (DomainHasConstraints(d->resulttype)) - return true; - expr = (Node *) d->arg; - } - else if (IsA(expr, FuncExpr)) - { - FuncExpr *f = (FuncExpr *) expr; ++ /* Append it to list of commands */ ++ newcmd = makeNode(AlterTableCmd); ++ newcmd->subtype = AT_ReAddComment; ++ newcmd->def = (Node *) cmd; ++ tab->subcmds[pass] = lappend(tab->subcmds[pass], newcmd); +} - switch (f->funcid) - { - case F_TIMESTAMPTZ_TIMESTAMP: - case F_TIMESTAMP_TIMESTAMPTZ: - if (TimestampTimestampTzRequiresRewrite()) - return true; - else - expr = linitial(f->args); - break; - default: - return true; - } +/* - * ALTER TABLE ENABLE/DISABLE RULE - * - * We just pass this off to rewriteDefine.c. ++ * Subroutine for ATPostAlterTypeParse(). Calls out to CheckIndexCompatible() ++ * for the real analysis, then mutates the IndexStmt based on that verdict. + */ +static void - ATExecEnableDisableRule(Relation rel, const char *rulename, - char fires_when, LOCKMODE lockmode) ++TryReuseIndex(Oid oldId, IndexStmt *stmt) +{ - EnableDisableRule(rel, rulename, fires_when); ++ if (CheckIndexCompatible(oldId, ++ stmt->accessMethod, ++ stmt->indexParams, ++ stmt->excludeOpNames, ++ stmt->iswithoutoverlaps)) ++ { ++ Relation irel = index_open(oldId, NoLock); + - InvokeObjectPostAlterHook(RelationRelationId, - RelationGetRelid(rel), 0); ++ /* If it's a partitioned index, there is no storage to share. */ ++ if (irel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX) ++ { ++ stmt->oldNumber = irel->rd_locator.relNumber; ++ stmt->oldCreateSubid = irel->rd_createSubid; ++ stmt->oldFirstRelfilelocatorSubid = irel->rd_firstRelfilelocatorSubid; + } - else - return true; ++ index_close(irel, NoLock); + } } /* - * ALTER TABLE INHERIT - * ALTER COLUMN .. SET DATA TYPE ++ * Subroutine for ATPostAlterTypeParse(). + * - * 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. ++ * Stash the old P-F equality operator into the Constraint node, for possible ++ * use by ATAddForeignKeyConstraint() in determining whether revalidation of ++ * this constraint can be skipped. + */ +static void - ATPrepAddInherit(Relation child_rel) ++TryReuseForeignKey(Oid oldId, Constraint *con) +{ - if (child_rel->rd_rel->reloftype) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot change inheritance of typed table"))); ++ HeapTuple tup; ++ Datum adatum; ++ ArrayType *arr; ++ Oid *rawarr; ++ int numkeys; ++ int i; + - if (child_rel->rd_rel->relispartition) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot change inheritance of a partition"))); ++ Assert(con->contype == CONSTR_FOREIGN); ++ Assert(con->old_conpfeqop == NIL); /* already prepared this node */ + - if (child_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot change inheritance of partitioned table"))); ++ tup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(oldId)); ++ if (!HeapTupleIsValid(tup)) /* should not happen */ ++ elog(ERROR, "cache lookup failed for constraint %u", oldId); ++ ++ adatum = SysCacheGetAttrNotNull(CONSTROID, tup, ++ Anum_pg_constraint_conpfeqop); ++ arr = DatumGetArrayTypeP(adatum); /* ensure not toasted */ ++ numkeys = ARR_DIMS(arr)[0]; ++ /* test follows the one in ri_FetchConstraintInfo() */ ++ if (ARR_NDIM(arr) != 1 || ++ ARR_HASNULL(arr) || ++ ARR_ELEMTYPE(arr) != OIDOID) ++ elog(ERROR, "conpfeqop is not a 1-D Oid array"); ++ rawarr = (Oid *) ARR_DATA_PTR(arr); ++ ++ /* stash a List of the operator Oids in our Constraint node */ ++ for (i = 0; i < numkeys; i++) ++ con->old_conpfeqop = lappend_oid(con->old_conpfeqop, rawarr[i]); ++ ++ ReleaseSysCache(tup); +} + +/* - * Return the address of the new parent relation. ++ * ALTER COLUMN .. OPTIONS ( ... ) + * - * Return the address of the modified column. ++ * Returns the address of the modified column */ static ObjectAddress - ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode) -ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, - AlterTableCmd *cmd, LOCKMODE lockmode) ++ATExecAlterColumnGenericOptions(Relation rel, ++ const char *colName, ++ List *options, ++ LOCKMODE lockmode) { - Relation parent_rel; - List *children; - char *colName = cmd->name; - ColumnDef *def = (ColumnDef *) cmd->def; - TypeName *typeName = def->typeName; - HeapTuple heapTup; - Form_pg_attribute attTup, - attOldTup; ++ Relation ftrel; ++ Relation attrel; ++ ForeignServer *server; ++ ForeignDataWrapper *fdw; ++ HeapTuple tuple; ++ HeapTuple newtuple; ++ bool isnull; ++ Datum repl_val[Natts_pg_attribute]; ++ bool repl_null[Natts_pg_attribute]; ++ bool repl_repl[Natts_pg_attribute]; ++ Datum datum; ++ Form_pg_foreign_table fttableform; ++ Form_pg_attribute atttableform; + AttrNumber attnum; - HeapTuple typeTuple; - Form_pg_type tform; - Oid targettype; - int32 targettypmod; - Oid targetcollid; - Node *defaultexpr; - Relation attrelation; - Relation depRel; - ScanKeyData key[3]; - SysScanDesc scan; - HeapTuple depTup; ObjectAddress address; - const char *trigger_name; -- /* - * A self-exclusive lock is needed here. See the similar case in - * MergeAttributes() for a full explanation. - * Clear all the missing values if we're rewriting the table, since this - * renders them pointless. -- */ - parent_rel = table_openrv(parent, ShareUpdateExclusiveLock); - if (tab->rewrite) - { - Relation newrel; ++ if (options == NIL) ++ return InvalidObjectAddress; + - newrel = table_open(RelationGetRelid(rel), NoLock); - RelationClearMissing(newrel); - relation_close(newrel, NoLock); - /* make sure we don't conflict with later attribute modifications */ - CommandCounterIncrement(); - } ++ /* First, determine FDW validator associated to the foreign table. */ ++ ftrel = table_open(ForeignTableRelationId, AccessShareLock); ++ tuple = SearchSysCache1(FOREIGNTABLEREL, ObjectIdGetDatum(rel->rd_id)); ++ if (!HeapTupleIsValid(tuple)) ++ ereport(ERROR, ++ (errcode(ERRCODE_UNDEFINED_OBJECT), ++ errmsg("foreign table \"%s\" does not exist", ++ RelationGetRelationName(rel)))); ++ fttableform = (Form_pg_foreign_table) GETSTRUCT(tuple); ++ server = GetForeignServer(fttableform->ftserver); ++ fdw = GetForeignDataWrapper(server->fdwid); - /* - * 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); - attrelation = table_open(AttributeRelationId, RowExclusiveLock); ++ table_close(ftrel, AccessShareLock); ++ ReleaseSysCache(tuple); - /* Permanent rels cannot inherit from temporary ones */ - if (parent_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP && - child_rel->rd_rel->relpersistence != RELPERSISTENCE_TEMP) - /* Look up the target column */ - heapTup = SearchSysCacheCopyAttName(RelationGetRelid(rel), colName); - if (!HeapTupleIsValid(heapTup)) /* shouldn't happen */ ++ attrel = table_open(AttributeRelationId, RowExclusiveLock); ++ tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName); ++ if (!HeapTupleIsValid(tuple)) ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot inherit from temporary relation \"%s\"", - RelationGetRelationName(parent_rel)))); + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + colName, RelationGetRelationName(rel)))); - attTup = (Form_pg_attribute) GETSTRUCT(heapTup); - attnum = attTup->attnum; - attOldTup = TupleDescAttr(tab->oldDesc, attnum - 1); - /* If parent rel is temp, it must belong to this session */ - if (RELATION_IS_OTHER_TEMP(parent_rel)) - /* Check for multiple ALTER TYPE on same column --- can't cope */ - if (attTup->atttypid != attOldTup->atttypid || - attTup->atttypmod != attOldTup->atttypmod) ++ /* Prevent them from altering a system attribute */ ++ atttableform = (Form_pg_attribute) GETSTRUCT(tuple); ++ attnum = atttableform->attnum; ++ if (attnum <= 0) ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot inherit from temporary relation of another session"))); + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot alter type of column \"%s\" twice", - colName))); ++ errmsg("cannot alter system column \"%s\"", colName))); - /* 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"))); - /* Look up the target type (should not fail, since prep found it) */ - typeTuple = typenameType(NULL, typeName, &targettypmod); - tform = (Form_pg_type) GETSTRUCT(typeTuple); - targettype = tform->oid; - /* And the collation */ - targetcollid = GetColumnDefCollation(NULL, def, targettype); - /* 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))); - /* - * If there is a default expression for the column, get it and ensure we - * can coerce it to the new datatype. (We must do this before changing - * the column type, because build_column_default itself will try to - * coerce, and will not issue the error message we want if it fails.) - * - * We remove any implicit coercion steps at the top level of the old - * default expression; this has been agreed to satisfy the principle of - * least surprise. (The conversion to the new column type should act like - * it started from what the user sees as the stored expression, and the - * implicit coercions aren't going to be shown.) - */ - if (attTup->atthasdef) - { - defaultexpr = build_column_default(rel, attnum); - Assert(defaultexpr); - defaultexpr = strip_implicit_coercions(defaultexpr); - defaultexpr = coerce_to_target_type(NULL, /* no UNKNOWN params */ - defaultexpr, exprType(defaultexpr), - targettype, targettypmod, - COERCION_ASSIGNMENT, - COERCE_IMPLICIT_CAST, - -1); - if (defaultexpr == NULL) - { - if (attTup->attgenerated) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("generation expression for column \"%s\" cannot be cast automatically to type %s", - colName, format_type_be(targettype)))); - else - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("default for column \"%s\" cannot be cast automatically to type %s", - colName, format_type_be(targettype)))); - } - } ++ /* Initialize buffers for new tuple values */ ++ memset(repl_val, 0, sizeof(repl_val)); ++ memset(repl_null, false, sizeof(repl_null)); ++ memset(repl_repl, false, sizeof(repl_repl)); + - /* Likewise for partitions */ - if (parent_rel->rd_rel->relispartition) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot inherit from a partition"))); ++ /* Extract the current options */ ++ datum = SysCacheGetAttr(ATTNAME, ++ tuple, ++ Anum_pg_attribute_attfdwoptions, ++ &isnull); ++ if (isnull) ++ datum = PointerGetDatum(NULL); + - /* - * 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); ++ /* Transform the options */ ++ datum = transformGenericOptions(AttributeRelationId, ++ datum, ++ options, ++ fdw->fdwvalidator); + - 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 (DatumGetPointer(datum) != NULL) ++ repl_val[Anum_pg_attribute_attfdwoptions - 1] = datum; + else - defaultexpr = NULL; ++ repl_null[Anum_pg_attribute_attfdwoptions - 1] = true; -- /* - * 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(). - * Find everything that depends on the column (constraints, indexes, etc), - * and record enough information to let us recreate the objects. - * - * The actual recreation does not happen here, but only after we have - * performed all the individual ALTER TYPE operations. We have to save - * the info before executing ALTER TYPE, though, else the deparser will - * get confused. -- */ - 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."))); - RememberAllDependentForRebuilding(tab, AT_AlterColumnType, rel, attnum, colName); ++ repl_repl[Anum_pg_attribute_attfdwoptions - 1] = true; - /* OK to create inheritance */ - CreateInheritance(child_rel, parent_rel, false); - /* - * Now scan for dependencies of this column on other things. The only - * things we should find are the dependency on the column datatype and - * possibly a collation dependency. Those can be removed. - */ - depRel = table_open(DependRelationId, RowExclusiveLock); ++ /* Everything looks good - update the tuple */ - ObjectAddressSet(address, RelationRelationId, - RelationGetRelid(parent_rel)); - ScanKeyInit(&key[0], - Anum_pg_depend_classid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(RelationRelationId)); - ScanKeyInit(&key[1], - Anum_pg_depend_objid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(RelationGetRelid(rel))); - ScanKeyInit(&key[2], - Anum_pg_depend_objsubid, - BTEqualStrategyNumber, F_INT4EQ, - Int32GetDatum((int32) attnum)); ++ newtuple = heap_modify_tuple(tuple, RelationGetDescr(attrel), ++ repl_val, repl_null, repl_repl); - /* keep our lock on the parent relation until commit */ - table_close(parent_rel, NoLock); - scan = systable_beginscan(depRel, DependDependerIndexId, true, - NULL, 3, key); ++ CatalogTupleUpdate(attrel, &newtuple->t_self, newtuple); + - while (HeapTupleIsValid(depTup = systable_getnext(scan))) - { - Form_pg_depend foundDep = (Form_pg_depend) GETSTRUCT(depTup); - ObjectAddress foundObject; ++ InvokeObjectPostAlterHook(RelationRelationId, ++ RelationGetRelid(rel), ++ atttableform->attnum); ++ ObjectAddressSubSet(address, RelationRelationId, ++ RelationGetRelid(rel), attnum); + - foundObject.classId = foundDep->refclassid; - foundObject.objectId = foundDep->refobjid; - foundObject.objectSubId = foundDep->refobjsubid; ++ ReleaseSysCache(tuple); + - if (foundDep->deptype != DEPENDENCY_NORMAL) - elog(ERROR, "found unexpected dependency type '%c'", - foundDep->deptype); - if (!(foundDep->refclassid == TypeRelationId && - foundDep->refobjid == attTup->atttypid) && - !(foundDep->refclassid == CollationRelationId && - foundDep->refobjid == attTup->attcollation)) - elog(ERROR, "found unexpected dependency for column: %s", - getObjectDescription(&foundObject, false)); ++ table_close(attrel, RowExclusiveLock); ++ ++ heap_freetuple(newtuple); + + return address; +} + +/* - * CreateInheritance - * Catalog manipulation portion of creating inheritance between a child - * table and a parent table. ++ * ALTER TABLE OWNER + * - * Common to ATExecAddInherit() and ATExecAttachPartition(). ++ * recursing is true if we are recursing from a table to its indexes, ++ * sequences, or toast table. We don't allow the ownership of those things to ++ * be changed separately from the parent table. Also, we can skip permission ++ * checks (this is necessary not just an optimization, else we'd fail to ++ * handle toast tables properly). ++ * ++ * recursing is also true if ALTER TYPE OWNER is calling us to fix up a ++ * free-standing composite type. + */ - static void - CreateInheritance(Relation child_rel, Relation parent_rel, bool ispartition) ++void ++ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lockmode) +{ - 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); ++ Relation target_rel; ++ Relation class_rel; ++ HeapTuple tuple; ++ Form_pg_class tuple_class; + + /* - * 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. ++ * Get exclusive lock till end of transaction on the target table. Use ++ * relation_open so that we can work on indexes and sequences. + */ - ScanKeyInit(&key, - Anum_pg_inherits_inhrelid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(RelationGetRelid(child_rel))); - scan = systable_beginscan(catalogRelation, InheritsRelidSeqnoIndexId, - true, NULL, 1, &key); ++ target_rel = relation_open(relationOid, lockmode); + - /* inhseqno sequences start at 1 */ - inhseqno = 0; - while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan))) - { - Form_pg_inherits inh = (Form_pg_inherits) GETSTRUCT(inheritsTuple); ++ /* Get its pg_class tuple, too */ ++ class_rel = table_open(RelationRelationId, RowExclusiveLock); - if (inh->inhparent == RelationGetRelid(parent_rel)) - CatalogTupleDelete(depRel, &depTup->t_self); - } ++ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relationOid)); ++ if (!HeapTupleIsValid(tuple)) ++ elog(ERROR, "cache lookup failed for relation %u", relationOid); ++ tuple_class = (Form_pg_class) GETSTRUCT(tuple); + - systable_endscan(scan); ++ /* Can we change the ownership of this tuple? */ ++ switch (tuple_class->relkind) ++ { ++ case RELKIND_RELATION: ++ case RELKIND_VIEW: ++ case RELKIND_MATVIEW: ++ case RELKIND_FOREIGN_TABLE: ++ case RELKIND_PARTITIONED_TABLE: ++ /* ok to change owner */ ++ break; ++ case RELKIND_INDEX: ++ if (!recursing) ++ { ++ /* ++ * Because ALTER INDEX OWNER used to be allowed, and in fact ++ * is generated by old versions of pg_dump, we give a warning ++ * and do nothing rather than erroring out. Also, to avoid ++ * unnecessary chatter while restoring those old dumps, say ++ * nothing at all if the command would be a no-op anyway. ++ */ ++ if (tuple_class->relowner != newOwnerId) ++ ereport(WARNING, ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("cannot change owner of index \"%s\"", ++ NameStr(tuple_class->relname)), ++ errhint("Change the ownership of the index's table instead."))); ++ /* quick hack to exit via the no-op path */ ++ newOwnerId = tuple_class->relowner; ++ } ++ break; ++ case RELKIND_PARTITIONED_INDEX: ++ if (recursing) ++ break; + ereport(ERROR, - (errcode(ERRCODE_DUPLICATE_TABLE), - errmsg("relation \"%s\" would be inherited from more than once", - RelationGetRelationName(parent_rel)))); ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("cannot change owner of index \"%s\"", ++ NameStr(tuple_class->relname)), ++ errhint("Change the ownership of the index's table instead."))); ++ break; ++ case RELKIND_SEQUENCE: ++ if (!recursing && ++ tuple_class->relowner != newOwnerId) ++ { ++ /* if it's an owned sequence, disallow changing it by itself */ ++ Oid tableId; ++ int32 colId; - if (inh->inhseqno > inhseqno) - inhseqno = inh->inhseqno; - table_close(depRel, RowExclusiveLock); ++ if (sequenceIsOwned(relationOid, DEPENDENCY_AUTO, &tableId, &colId) || ++ sequenceIsOwned(relationOid, DEPENDENCY_INTERNAL, &tableId, &colId)) ++ ereport(ERROR, ++ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ++ errmsg("cannot change owner of sequence \"%s\"", ++ NameStr(tuple_class->relname)), ++ errdetail("Sequence \"%s\" is linked to table \"%s\".", ++ NameStr(tuple_class->relname), ++ get_rel_name(tableId)))); ++ } ++ break; ++ case RELKIND_COMPOSITE_TYPE: ++ if (recursing) ++ break; ++ ereport(ERROR, ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("\"%s\" is a composite type", ++ NameStr(tuple_class->relname)), ++ /* translator: %s is an SQL ALTER command */ ++ errhint("Use %s instead.", ++ "ALTER TYPE"))); ++ break; ++ case RELKIND_TOASTVALUE: ++ if (recursing) ++ break; ++ /* FALL THRU */ ++ default: ++ ereport(ERROR, ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("cannot change owner of relation \"%s\"", ++ NameStr(tuple_class->relname)), ++ errdetail_relkind_not_supported(tuple_class->relkind))); + } - 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. - * Here we go --- change the recorded column type and collation. (Note - * heapTup is a copy of the syscache entry, so okay to scribble on.) First - * fix up the missing value if any. ++ * If the new owner is the same as the existing owner, consider the ++ * command to have succeeded. This is for dump restoration purposes. */ - 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); - } - if (attTup->atthasmissing) ++ if (tuple_class->relowner != newOwnerId) + { - Datum missingval; - bool missingNull; - - /* if rewrite is true the missing value should already be cleared */ - Assert(tab->rewrite == 0); - - /* Get the missing value datum */ - missingval = heap_getattr(heapTup, - Anum_pg_attribute_attmissingval, - attrelation->rd_att, - &missingNull); - - /* if it's a null array there is nothing to do */ ++ Datum repl_val[Natts_pg_class]; ++ bool repl_null[Natts_pg_class]; ++ bool repl_repl[Natts_pg_class]; ++ Acl *newAcl; ++ Datum aclDatum; ++ bool isNull; ++ HeapTuple newtuple; - /* - * 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 (!missingNull) ++ /* skip permission checks when recursing to index or toast table */ ++ if (!recursing) + { - /* - * Get the datum out of the array and repack it in a new array - * built with the new type data. We assume that since the table - * doesn't need rewriting, the actual Datum doesn't need to be - * changed, only the array metadata. - */ - - int one = 1; - bool isNull; - Datum valuesAtt[Natts_pg_attribute] = {0}; - bool nullsAtt[Natts_pg_attribute] = {0}; - bool replacesAtt[Natts_pg_attribute] = {0}; - HeapTuple newTup; ++ /* Superusers can always do it */ ++ if (!superuser()) ++ { ++ Oid namespaceOid = tuple_class->relnamespace; ++ AclResult aclresult; - if (acon->condeferrable != bcon->condeferrable || - acon->condeferred != bcon->condeferred || - strcmp(decompile_conbin(a, tupleDesc), - decompile_conbin(b, tupleDesc)) != 0) - return false; - else - return true; - } - missingval = array_get_element(missingval, - 1, - &one, - 0, - attTup->attlen, - attTup->attbyval, - attTup->attalign, - &isNull); - missingval = PointerGetDatum(construct_array(&missingval, - 1, - targettype, - tform->typlen, - tform->typbyval, - tform->typalign)); ++ /* Otherwise, must be owner of the existing object */ ++ if (!object_ownercheck(RelationRelationId, relationOid, GetUserId())) ++ aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relationOid)), ++ RelationGetRelationName(target_rel)); - /* - * 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; - valuesAtt[Anum_pg_attribute_attmissingval - 1] = missingval; - replacesAtt[Anum_pg_attribute_attmissingval - 1] = true; - nullsAtt[Anum_pg_attribute_attmissingval - 1] = false; ++ /* Must be able to become new owner */ ++ check_can_set_role(GetUserId(), newOwnerId); - attrrel = table_open(AttributeRelationId, RowExclusiveLock); - parent_desc = RelationGetDescr(parent_rel); - newTup = heap_modify_tuple(heapTup, RelationGetDescr(attrelation), - valuesAtt, nullsAtt, replacesAtt); - heap_freetuple(heapTup); - heapTup = newTup; - attTup = (Form_pg_attribute) GETSTRUCT(heapTup); ++ /* New owner must have CREATE privilege on namespace */ ++ aclresult = object_aclcheck(NamespaceRelationId, namespaceOid, newOwnerId, ++ ACL_CREATE); ++ if (aclresult != ACLCHECK_OK) ++ aclcheck_error(aclresult, OBJECT_SCHEMA, ++ get_namespace_name(namespaceOid)); ++ } + } - } - - attTup->atttypid = targettype; - attTup->atttypmod = targettypmod; - attTup->attcollation = targetcollid; - if (list_length(typeName->arrayBounds) > PG_INT16_MAX) - ereport(ERROR, - errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("too many array dimensions")); - attTup->attndims = list_length(typeName->arrayBounds); - attTup->attlen = tform->typlen; - attTup->attbyval = tform->typbyval; - attTup->attalign = tform->typalign; - attTup->attstorage = tform->typstorage; - attTup->attcompression = InvalidCompressionMethod; - 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; - ReleaseSysCache(typeTuple); ++ memset(repl_null, false, sizeof(repl_null)); ++ memset(repl_repl, false, sizeof(repl_repl)); - /* Ignore dropped columns in the parent. */ - if (parent_att->attisdropped) - continue; - CatalogTupleUpdate(attrelation, &heapTup->t_self, heapTup); ++ repl_repl[Anum_pg_class_relowner - 1] = true; ++ repl_val[Anum_pg_class_relowner - 1] = ObjectIdGetDatum(newOwnerId); - /* Find same column in child (matching on column name). */ - tuple = SearchSysCacheCopyAttName(RelationGetRelid(child_rel), parent_attname); - if (HeapTupleIsValid(tuple)) - table_close(attrelation, RowExclusiveLock); ++ /* ++ * Determine the modified ACL for the new owner. This is only ++ * necessary when the ACL is non-null. ++ */ ++ aclDatum = SysCacheGetAttr(RELOID, tuple, ++ Anum_pg_class_relacl, ++ &isNull); ++ if (!isNull) + { - 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))); ++ newAcl = aclnewowner(DatumGetAclP(aclDatum), ++ tuple_class->relowner, newOwnerId); ++ repl_repl[Anum_pg_class_relacl - 1] = true; ++ repl_val[Anum_pg_class_relacl - 1] = PointerGetDatum(newAcl); ++ } - 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))); - /* Install dependencies on new datatype and collation */ - add_column_datatype_dependency(RelationGetRelid(rel), attnum, targettype); - add_column_collation_dependency(RelationGetRelid(rel), attnum, targetcollid); ++ newtuple = heap_modify_tuple(tuple, RelationGetDescr(class_rel), repl_val, repl_null, repl_repl); - /* - * 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; - /* - * Drop any pg_statistic entry for the column, since it's now wrong type - */ - RemoveStatistics(RelationGetRelid(rel), attnum); ++ CatalogTupleUpdate(class_rel, &newtuple->t_self, newtuple); - 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))); - } - InvokeObjectPostAlterHook(RelationRelationId, - RelationGetRelid(rel), attnum); ++ heap_freetuple(newtuple); - /* - * 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))); - /* - * Update the default, if present, by brute force --- remove and re-add - * the default. Probably unsafe to take shortcuts, since the new version - * may well have additional dependencies. (It's okay to do this now, - * rather than after other ALTER TYPE commands, since the default won't - * depend on other column types.) - */ - if (defaultexpr) - { + /* - * If it's a GENERATED default, drop its dependency records, in - * particular its INTERNAL dependency on the column, which would - * otherwise cause dependency.c to refuse to perform the deletion. ++ * We must similarly update any per-column ACLs to reflect the new ++ * owner; for neatness reasons that's split out as a subroutine. + */ - if (attTup->attgenerated) - { - Oid attrdefoid = GetAttrDefaultOid(RelationGetRelid(rel), attnum); ++ change_owner_fix_column_acls(relationOid, ++ tuple_class->relowner, ++ newOwnerId); - 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"))); - if (!OidIsValid(attrdefoid)) - elog(ERROR, "could not find attrdef tuple for relation %u attnum %d", - RelationGetRelid(rel), attnum); - (void) deleteDependencyRecordsFor(AttrDefaultRelationId, attrdefoid, false); - } ++ /* ++ * Update owner dependency reference, if any. A composite type has ++ * none, because it's tracked for the pg_type entry instead of here; ++ * indexes and TOAST tables don't have their own entries either. ++ */ ++ if (tuple_class->relkind != RELKIND_COMPOSITE_TYPE && ++ tuple_class->relkind != RELKIND_INDEX && ++ tuple_class->relkind != RELKIND_PARTITIONED_INDEX && ++ tuple_class->relkind != RELKIND_TOASTVALUE) ++ changeDependencyOnOwner(RelationRelationId, relationOid, ++ newOwnerId); - /* - * 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; + /* - * Make updates-so-far visible, particularly the new pg_attribute row - * which will be updated again. ++ * Also change the ownership of the table's row type, if it has one + */ - CommandCounterIncrement(); ++ if (OidIsValid(tuple_class->reltype)) ++ AlterTypeOwnerInternal(tuple_class->reltype, newOwnerId); - /* - * 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")); + /* - * We use RESTRICT here for safety, but at present we do not expect - * anything to depend on the default. ++ * If we are operating on a table or materialized view, also change ++ * the ownership of any indexes and sequences that belong to the ++ * relation, as well as its toast table (if it has one). + */ - RemoveAttrDefault(RelationGetRelid(rel), attnum, DROP_RESTRICT, true, - true); ++ if (tuple_class->relkind == RELKIND_RELATION || ++ tuple_class->relkind == RELKIND_PARTITIONED_TABLE || ++ tuple_class->relkind == RELKIND_MATVIEW || ++ tuple_class->relkind == RELKIND_TOASTVALUE) ++ { ++ List *index_oid_list; ++ ListCell *i; - /* - * 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; - } - (void) StoreAttrDefault(rel, attnum, defaultexpr, true); - } ++ /* Find all the indexes belonging to this relation */ ++ index_oid_list = RelationGetIndexList(target_rel); - 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))); - ObjectAddressSubSet(address, RelationRelationId, - RelationGetRelid(rel), attnum); ++ /* For each index, recursively change its ownership */ ++ foreach(i, index_oid_list) ++ ATExecChangeOwner(lfirst_oid(i), newOwnerId, true, lockmode); + - /* Cleanup */ - heap_freetuple(heapTup); ++ list_free(index_oid_list); + } + - return address; ++ /* If it has a toast table, recurse to change its ownership */ ++ if (tuple_class->reltoastrelid != InvalidOid) ++ ATExecChangeOwner(tuple_class->reltoastrelid, newOwnerId, ++ true, lockmode); ++ ++ /* If it has dependent sequences, recurse to change them too */ ++ change_owner_recurse_to_sequences(relationOid, newOwnerId, lockmode); + } + - table_close(attrrel, RowExclusiveLock); ++ InvokeObjectPostAlterHook(RelationRelationId, relationOid, 0); ++ ++ ReleaseSysCache(tuple); ++ table_close(class_rel, RowExclusiveLock); ++ relation_close(target_rel, NoLock); } /* - * 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. - * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything - * that depends on the column (constraints, indexes, etc), and record enough - * information to let us recreate the objects. ++ * change_owner_fix_column_acls + * - * XXX See MergeWithExistingConstraint too if you change this code. ++ * Helper function for ATExecChangeOwner. Scan the columns of the table ++ * and fix any non-null column ACLs to reflect the new owner. */ static void - MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel) -RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, - Relation rel, AttrNumber attnum, const char *colName) ++change_owner_fix_column_acls(Oid relationOid, Oid oldOwnerId, Oid newOwnerId) { - Relation constraintrel; - SysScanDesc parent_scan; - ScanKeyData parent_key; - HeapTuple parent_tuple; - Oid parent_relid = RelationGetRelid(parent_rel); - AttrMap *attmap; - - constraintrel = table_open(ConstraintRelationId, RowExclusiveLock); - Relation depRel; - ScanKeyData key[3]; ++ Relation attRelation; + SysScanDesc scan; - HeapTuple depTup; - - Assert(subtype == AT_AlterColumnType || subtype == AT_SetExpression); - - depRel = table_open(DependRelationId, RowExclusiveLock); ++ ScanKeyData key[1]; ++ HeapTuple attributeTuple; - /* Outer loop scans through the parent's constraint definitions */ - ScanKeyInit(&parent_key, - Anum_pg_constraint_conrelid, ++ attRelation = table_open(AttributeRelationId, RowExclusiveLock); + ScanKeyInit(&key[0], - Anum_pg_depend_refclassid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(RelationRelationId)); - ScanKeyInit(&key[1], - Anum_pg_depend_refobjid, ++ Anum_pg_attribute_attrelid, BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(parent_relid)); - parent_scan = systable_beginscan(constraintrel, ConstraintRelidTypidNameIndexId, - true, NULL, 1, &parent_key); - ObjectIdGetDatum(RelationGetRelid(rel))); - ScanKeyInit(&key[2], - Anum_pg_depend_refobjsubid, - BTEqualStrategyNumber, F_INT4EQ, - Int32GetDatum((int32) attnum)); -- - attmap = build_attrmap_by_name(RelationGetDescr(parent_rel), - RelationGetDescr(child_rel), - true); - scan = systable_beginscan(depRel, DependReferenceIndexId, true, - NULL, 3, key); -- - while (HeapTupleIsValid(parent_tuple = systable_getnext(parent_scan))) - while (HeapTupleIsValid(depTup = systable_getnext(scan))) ++ ObjectIdGetDatum(relationOid)); ++ scan = systable_beginscan(attRelation, AttributeRelidNumIndexId, ++ true, NULL, 1, key); ++ while (HeapTupleIsValid(attributeTuple = systable_getnext(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; - Form_pg_depend foundDep = (Form_pg_depend) GETSTRUCT(depTup); - ObjectAddress foundObject; - - foundObject.classId = foundDep->classid; - foundObject.objectId = foundDep->objid; - foundObject.objectSubId = foundDep->objsubid; - - switch (foundObject.classId) - { - case RelationRelationId: - { - char relKind = get_rel_relkind(foundObject.objectId); - - if (relKind == RELKIND_INDEX || - relKind == RELKIND_PARTITIONED_INDEX) - { - Assert(foundObject.objectSubId == 0); - RememberIndexForRebuilding(foundObject.objectId, tab); - } - else if (relKind == RELKIND_SEQUENCE) - { - /* - * This must be a SERIAL column's sequence. We need - * not do anything to it. - */ - Assert(foundObject.objectSubId == 0); - } - else - { - /* Not expecting any other direct dependencies... */ - elog(ERROR, "unexpected object depending on column: %s", - getObjectDescription(&foundObject, false)); - } - break; - } - - case ConstraintRelationId: - Assert(foundObject.objectSubId == 0); - RememberConstraintForRebuilding(foundObject.objectId, tab); - break; - - case ProcedureRelationId: - - /* - * A new-style SQL function can depend on a column, if that - * column is referenced in the parsed function body. Ideally - * we'd automatically update the function by deparsing and - * reparsing it, but that's risky and might well fail anyhow. - * FIXME someday. - * - * This is only a problem for AT_AlterColumnType, not - * AT_SetExpression. - */ - if (subtype == AT_AlterColumnType) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot alter type of a column used by a function or procedure"), - errdetail("%s depends on column \"%s\"", - getObjectDescription(&foundObject, false), - colName))); - break; - - case RewriteRelationId: - - /* - * View/rule bodies have pretty much the same issues as - * function bodies. FIXME someday. - */ - if (subtype == AT_AlterColumnType) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot alter type of a column used by a view or rule"), - errdetail("%s depends on column \"%s\"", - getObjectDescription(&foundObject, false), - colName))); - break; - - case TriggerRelationId: ++ Form_pg_attribute att = (Form_pg_attribute) GETSTRUCT(attributeTuple); ++ Datum repl_val[Natts_pg_attribute]; ++ bool repl_null[Natts_pg_attribute]; ++ bool repl_repl[Natts_pg_attribute]; ++ Acl *newAcl; ++ Datum aclDatum; ++ bool isNull; ++ HeapTuple newtuple; - if (parent_con->contype != CONSTRAINT_CHECK && - parent_con->contype != CONSTRAINT_NOTNULL) - /* - * A trigger can depend on a column because the column is - * specified as an update target, or because the column is - * used in the trigger's WHEN condition. The first case would - * not require any extra work, but the second case would - * require updating the WHEN expression, which has the same - * issues as above. Since we can't easily tell which case - * applies, we punt for both. FIXME someday. - */ - if (subtype == AT_AlterColumnType) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot alter type of a column used in a trigger definition"), - errdetail("%s depends on column \"%s\"", - getObjectDescription(&foundObject, false), - colName))); - break; ++ /* Ignore dropped columns */ ++ if (att->attisdropped) + continue; - /* if the parent's constraint is marked NO INHERIT, it's not inherited */ - if (parent_con->connoinherit) - case PolicyRelationId: ++ aclDatum = heap_getattr(attributeTuple, ++ Anum_pg_attribute_attacl, ++ RelationGetDescr(attRelation), ++ &isNull); ++ /* Null ACLs do not require changes */ ++ if (isNull) + 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"); - } - /* - * A policy can depend on a column because the column is - * specified in the policy's USING or WITH CHECK qual - * expressions. It might be possible to rewrite and recheck - * the policy expression, but punt for now. It's certainly - * easy enough to remove and recreate the policy; still, FIXME - * someday. - */ - if (subtype == AT_AlterColumnType) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot alter type of a column used in a policy definition"), - errdetail("%s depends on column \"%s\"", - getObjectDescription(&foundObject, false), - colName))); - break; ++ memset(repl_null, false, sizeof(repl_null)); ++ memset(repl_repl, false, sizeof(repl_repl)); - 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)))); - case AttrDefaultRelationId: - { - ObjectAddress col = GetAttrDefaultColumnAddress(foundObject.objectId); ++ newAcl = aclnewowner(DatumGetAclP(aclDatum), ++ oldOwnerId, newOwnerId); ++ repl_repl[Anum_pg_attribute_attacl - 1] = true; ++ repl_val[Anum_pg_attribute_attacl - 1] = PointerGetDatum(newAcl); - /* - * 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 (col.objectId == RelationGetRelid(rel) && - col.objectSubId == attnum) - { - /* - * Ignore the column's own default expression. The - * caller deals with it. - */ - } - else - { - /* - * This must be a reference from the expression of a - * generated column elsewhere in the same table. - * Changing the type/generated expression of a column - * that is used by a generated column is not allowed - * by SQL standard, so just punt for now. It might be - * doable with some thinking and effort. - */ - if (subtype == AT_AlterColumnType) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot alter type of a column used by a generated column"), - errdetail("Column \"%s\" is used by generated column \"%s\".", - colName, - get_attname(col.objectId, - col.objectSubId, - false)))); - } - break; - } ++ newtuple = heap_modify_tuple(attributeTuple, ++ RelationGetDescr(attRelation), ++ repl_val, repl_null, repl_repl); - /* - * 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)))); - case StatisticExtRelationId: ++ CatalogTupleUpdate(attRelation, &newtuple->t_self, newtuple); - /* - * 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)))); - /* - * Give the extended-stats machinery a chance to fix anything - * that this column type change would break. - */ - RememberStatisticsForRebuilding(foundObject.objectId, tab); - break; ++ heap_freetuple(newtuple); ++ } ++ systable_endscan(scan); ++ table_close(attRelation, RowExclusiveLock); ++} - /* - * 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); - case PublicationRelRelationId: ++/* ++ * change_owner_recurse_to_sequences ++ * ++ * Helper function for ATExecChangeOwner. Examines pg_depend searching ++ * for sequences that are dependent on serial columns, and changes their ++ * ownership. ++ */ ++static void ++change_owner_recurse_to_sequences(Oid relationOid, Oid newOwnerId, LOCKMODE lockmode) ++{ ++ Relation depRel; ++ SysScanDesc scan; ++ ScanKeyData key[2]; ++ HeapTuple tup; - if (pg_add_s16_overflow(child_con->coninhcount, 1, - &child_con->coninhcount)) - ereport(ERROR, - errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("too many inheritance parents")); - /* - * Column reference in a PUBLICATION ... FOR TABLE ... WHERE - * clause. Same issues as above. FIXME someday. - */ - if (subtype == AT_AlterColumnType) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot alter type of a column used by a publication WHERE clause"), - errdetail("%s depends on column \"%s\"", - getObjectDescription(&foundObject, false), - colName))); - break; ++ /* ++ * 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); - /* - * 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; - } - default: ++ ScanKeyInit(&key[0], ++ Anum_pg_depend_refclassid, ++ BTEqualStrategyNumber, F_OIDEQ, ++ ObjectIdGetDatum(RelationRelationId)); ++ ScanKeyInit(&key[1], ++ Anum_pg_depend_refobjid, ++ BTEqualStrategyNumber, F_OIDEQ, ++ ObjectIdGetDatum(relationOid)); ++ /* we leave refobjsubid unspecified */ - CatalogTupleUpdate(constraintrel, &child_copy->t_self, child_copy); - heap_freetuple(child_copy); - /* - * We don't expect any other sorts of objects to depend on a - * column. - */ - elog(ERROR, "unexpected object depending on column: %s", - getObjectDescription(&foundObject, false)); - break; ++ scan = systable_beginscan(depRel, DependReferenceIndexId, true, ++ NULL, 2, key); + - found = true; - break; - } ++ while (HeapTupleIsValid(tup = systable_getnext(scan))) ++ { ++ Form_pg_depend depForm = (Form_pg_depend) GETSTRUCT(tup); ++ Relation seqRel; + - systable_endscan(child_scan); ++ /* 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; + - 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))); ++ /* Use relation_open just in case it's an index */ ++ seqRel = relation_open(depForm->objid, lockmode); + - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("child table is missing constraint \"%s\"", - NameStr(parent_con->conname)))); ++ /* skip non-sequence relations */ ++ if (RelationGetForm(seqRel)->relkind != RELKIND_SEQUENCE) ++ { ++ /* No need to keep the lock */ ++ relation_close(seqRel, lockmode); ++ continue; } ++ ++ /* We don't need to close the sequence while we alter it. */ ++ ATExecChangeOwner(depForm->objid, newOwnerId, true, lockmode); ++ ++ /* Now we can close it. Keep the lock till end of transaction. */ ++ relation_close(seqRel, NoLock); } - systable_endscan(parent_scan); - table_close(constraintrel, RowExclusiveLock); + systable_endscan(scan); - table_close(depRel, NoLock); ++ ++ relation_close(depRel, AccessShareLock); } /* - * ALTER TABLE NO INHERIT - * Subroutine for ATExecAlterColumnType: remember that a replica identity - * needs to be reset. ++ * ALTER TABLE CLUSTER ON + * - * Return value is the address of the relation that is no longer parent. ++ * The only thing we have to do is to change the indisclustered bits. ++ * ++ * Return the address of the new clustering index. */ -static void -RememberReplicaIdentityForRebuilding(Oid indoid, AlteredTableInfo *tab) +static ObjectAddress - ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode) ++ATExecClusterOn(Relation rel, const char *indexName, LOCKMODE lockmode) { - if (!get_index_isreplident(indoid)) - return; ++ Oid indexOid; + ObjectAddress address; - Relation parent_rel; - - if (rel->rd_rel->relispartition) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot change inheritance of a partition"))); - /* - * 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); - if (tab->replicaIdentityIndex) - elog(ERROR, "relation %u has multiple indexes marked as replica identity", tab->relid); ++ indexOid = get_relname_relid(indexName, rel->rd_rel->relnamespace); - /* - * We don't bother to check ownership of the parent table --- ownership of - * the child is presumed enough rights. - */ - tab->replicaIdentityIndex = get_rel_name(indoid); ++ if (!OidIsValid(indexOid)) ++ ereport(ERROR, ++ (errcode(ERRCODE_UNDEFINED_OBJECT), ++ errmsg("index \"%s\" for table \"%s\" does not exist", ++ indexName, RelationGetRelationName(rel)))); + - /* Off to RemoveInheritance() where most of the work happens */ - RemoveInheritance(rel, parent_rel, false); ++ /* Check index is valid to cluster on */ ++ check_index_is_clusterable(rel, indexOid, lockmode); + - ObjectAddressSet(address, RelationRelationId, - RelationGetRelid(parent_rel)); ++ /* And do the work */ ++ mark_index_clustered(rel, indexOid, false); + - /* keep our lock on the parent relation until commit */ - table_close(parent_rel, NoLock); ++ ObjectAddressSet(address, ++ RelationRelationId, indexOid); + + return address; } /* - * MarkInheritDetached - * Subroutine for ATExecAlterColumnType: remember any clustered index. ++ * ALTER TABLE SET WITHOUT CLUSTER + * - * Set inhdetachpending for a partition, for ATExecDetachPartition - * in concurrent mode. While at it, verify that no other partition is - * already pending detach. ++ * We have to find any indexes on the table that have indisclustered bit ++ * set and turn it off. */ static void - MarkInheritDetached(Relation child_rel, Relation parent_rel) -RememberClusterOnForRebuilding(Oid indoid, AlteredTableInfo *tab) ++ATExecDropCluster(Relation rel, LOCKMODE lockmode) { - Relation catalogRelation; - SysScanDesc scan; - ScanKeyData key; - HeapTuple inheritsTuple; - bool found = false; - if (!get_index_isclustered(indoid)) - return; - - if (tab->clusterOnIndex) - elog(ERROR, "relation %u has multiple clustered indexes", tab->relid); - - tab->clusterOnIndex = get_rel_name(indoid); ++ mark_index_clustered(rel, InvalidOid, false); + } - Assert(parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); + /* - * Subroutine for ATExecAlterColumnType: remember that a constraint needs - * to be rebuilt (which we might already know). ++ * Preparation phase for SET ACCESS METHOD ++ * ++ * Check that the access method exists and determine whether a change is ++ * actually needed. + */ + static void -RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab) ++ATPrepSetAccessMethod(AlteredTableInfo *tab, Relation rel, const char *amname) + { ++ Oid amoid; + /* - * Find pg_inherits entries by inhparent. (We need to scan them all in - * order to verify that no other partition is pending detach.) - * This de-duplication check is critical for two independent reasons: we - * mustn't try to recreate the same constraint twice, and if a constraint - * depends on more than one column whose type is to be altered, we must - * capture its definition string before applying any of the column type - * changes. ruleutils.c will get confused if we ask again later. ++ * Look up the access method name and check that it differs from the ++ * table's current AM. If DEFAULT was specified for a partitioned table ++ * (amname is NULL), set it to InvalidOid to reset the catalogued AM. */ - 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); - if (!list_member_oid(tab->changedConstraintOids, conoid)) - { - /* OK, capture the constraint's existing definition string */ - char *defstring = pg_get_constraintdef_command(conoid); - Oid indoid; - - /* - * It is critical to create not-null constraints ahead of primary key - * indexes; otherwise, the not-null constraint would be created by the - * primary key, and the constraint name would be wrong. - */ - if (get_constraint_type(conoid) == CONSTRAINT_NOTNULL) - { - tab->changedConstraintOids = lcons_oid(conoid, - tab->changedConstraintOids); - tab->changedConstraintDefs = lcons(defstring, - tab->changedConstraintDefs); - } - else - { ++ if (amname != NULL) ++ amoid = get_table_am_oid(amname, false); ++ else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) ++ amoid = InvalidOid; ++ else ++ amoid = get_table_am_oid(default_table_access_method, false); - while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan))) - tab->changedConstraintOids = lappend_oid(tab->changedConstraintOids, - conoid); - tab->changedConstraintDefs = lappend(tab->changedConstraintDefs, - defstring); - } ++ /* if it's a match, phase 3 doesn't need to do anything */ ++ if (rel->rd_rel->relam == amoid) ++ return; + - /* - * For the index of a constraint, if any, remember if it is used for - * the table's replica identity or if it is a clustered index, so that - * ATPostAlterTypeCleanup() can queue up commands necessary to restore - * those properties. - */ - indoid = get_constraint_index(conoid); - if (OidIsValid(indoid)) - { - RememberReplicaIdentityForRebuilding(indoid, tab); - RememberClusterOnForRebuilding(indoid, tab); - } - } ++ /* Save info for Phase 3 to do the real work */ ++ tab->rewrite |= AT_REWRITE_ACCESS_METHOD; ++ tab->newAccessMethod = amoid; ++ tab->chgAccessMethod = true; + } + + /* - * Subroutine for ATExecAlterColumnType: remember that an index needs - * to be rebuilt (which we might already know). ++ * Special handling of ALTER TABLE SET ACCESS METHOD for relations with no ++ * storage that have an interest in preserving AM. ++ * ++ * Since these have no storage, setting the access method is a catalog only ++ * operation. + */ + static void -RememberIndexForRebuilding(Oid indoid, AlteredTableInfo *tab) ++ATExecSetAccessMethodNoStorage(Relation rel, Oid newAccessMethodId) + { ++ Relation pg_class; ++ Oid oldAccessMethodId; ++ HeapTuple tuple; ++ Form_pg_class rd_rel; ++ Oid reloid = RelationGetRelid(rel); ++ ++ /* ++ * Shouldn't be called on relations having storage; these are processed in ++ * phase 3. ++ */ ++ Assert(!RELKIND_HAS_STORAGE(rel->rd_rel->relkind)); ++ ++ /* Get a modifiable copy of the relation's pg_class row. */ ++ pg_class = table_open(RelationRelationId, RowExclusiveLock); ++ ++ tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid)); ++ if (!HeapTupleIsValid(tuple)) ++ elog(ERROR, "cache lookup failed for relation %u", reloid); ++ rd_rel = (Form_pg_class) GETSTRUCT(tuple); ++ ++ /* Update the pg_class row. */ ++ oldAccessMethodId = rd_rel->relam; ++ rd_rel->relam = newAccessMethodId; ++ ++ /* Leave if no update required */ ++ if (rd_rel->relam == oldAccessMethodId) + { - Form_pg_inherits inhForm; ++ heap_freetuple(tuple); ++ table_close(pg_class, RowExclusiveLock); ++ return; ++ } + - 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.")); ++ CatalogTupleUpdate(pg_class, &tuple->t_self, tuple); + - if (inhForm->inhrelid == RelationGetRelid(child_rel)) - { - HeapTuple newtup; + /* - * This de-duplication check is critical for two independent reasons: we - * mustn't try to recreate the same index twice, and if an index depends - * on more than one column whose type is to be altered, we must capture - * its definition string before applying any of the column type changes. - * ruleutils.c will get confused if we ask again later. ++ * Update the dependency on the new access method. No dependency is added ++ * if the new access method is InvalidOid (default case). Be very careful ++ * that this has to compare the previous value stored in pg_class with the ++ * new one. + */ - if (!list_member_oid(tab->changedIndexOids, indoid)) ++ if (!OidIsValid(oldAccessMethodId) && OidIsValid(rd_rel->relam)) + { ++ ObjectAddress relobj, ++ referenced; + - newtup = heap_copytuple(inheritsTuple); - ((Form_pg_inherits) GETSTRUCT(newtup))->inhdetachpending = true; + /* - * Before adding it as an index-to-rebuild, we'd better see if it - * belongs to a constraint, and if so rebuild the constraint instead. - * Typically this check fails, because constraint indexes normally - * have only dependencies on their constraint. But it's possible for - * such an index to also have direct dependencies on table columns, - * for example with a partial exclusion constraint. ++ * New access method is defined and there was no dependency ++ * previously, so record a new one. + */ - Oid conoid = get_index_constraint(indoid); ++ ObjectAddressSet(relobj, RelationRelationId, reloid); ++ ObjectAddressSet(referenced, AccessMethodRelationId, rd_rel->relam); ++ recordDependencyOn(&relobj, &referenced, DEPENDENCY_NORMAL); ++ } ++ else if (OidIsValid(oldAccessMethodId) && ++ !OidIsValid(rd_rel->relam)) ++ { ++ /* ++ * There was an access method defined, and no new one, so just remove ++ * the existing dependency. ++ */ ++ deleteDependencyRecordsForClass(RelationRelationId, reloid, ++ AccessMethodRelationId, ++ DEPENDENCY_NORMAL); ++ } ++ else ++ { ++ Assert(OidIsValid(oldAccessMethodId) && ++ OidIsValid(rd_rel->relam)); - CatalogTupleUpdate(catalogRelation, - &inheritsTuple->t_self, - newtup); - found = true; - heap_freetuple(newtup); - /* keep looking, to ensure we catch others pending detach */ - if (OidIsValid(conoid)) - { - RememberConstraintForRebuilding(conoid, tab); -- } - else - { - /* OK, capture the index's existing definition string */ - char *defstring = pg_get_indexdef_string(indoid); ++ /* Both are valid, so update the dependency */ ++ changeDependencyFor(RelationRelationId, reloid, ++ AccessMethodRelationId, ++ oldAccessMethodId, rd_rel->relam); + } - /* Done */ - systable_endscan(scan); - table_close(catalogRelation, RowExclusiveLock); - tab->changedIndexOids = lappend_oid(tab->changedIndexOids, - indoid); - tab->changedIndexDefs = lappend(tab->changedIndexDefs, - defstring); ++ /* make the relam and dependency changes visible */ ++ CommandCounterIncrement(); - if (!found) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_TABLE), - errmsg("relation \"%s\" is not a partition of relation \"%s\"", - RelationGetRelationName(child_rel), - RelationGetRelationName(parent_rel)))); - } - /* - * Remember if this index is used for the table's replica identity - * or if it is a clustered index, so that ATPostAlterTypeCleanup() - * can queue up commands necessary to restore those properties. - */ - RememberReplicaIdentityForRebuilding(indoid, tab); - RememberClusterOnForRebuilding(indoid, tab); - } - } ++ InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0); + - /* - * 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(). ++ heap_freetuple(tuple); ++ table_close(pg_class, RowExclusiveLock); + } + + /* - * Subroutine for ATExecAlterColumnType: remember that a statistics object - * needs to be rebuilt (which we might already know). ++ * ALTER TABLE SET TABLESPACE */ static void - RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached) -RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab) ++ATPrepSetTableSpace(AlteredTableInfo *tab, Relation rel, const char *tablespacename, LOCKMODE lockmode) { - 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); - /* - * This de-duplication check is critical for two independent reasons: we - * mustn't try to recreate the same statistics object twice, and if the - * statistics object depends on more than one column whose type is to be - * altered, we must capture its definition string before applying any of - * the type changes. ruleutils.c will get confused if we ask again later. - */ - if (!list_member_oid(tab->changedStatisticsOids, stxoid)) ++ Oid tablespaceId; + - 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)))); - } ++ /* Check that the tablespace exists */ ++ tablespaceId = get_tablespace_oid(tablespacename, false); + - /* - * 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))) ++ /* Check permissions except when moving to database's default */ ++ if (OidIsValid(tablespaceId) && tablespaceId != MyDatabaseTableSpace) { - Form_pg_attribute att = (Form_pg_attribute) GETSTRUCT(attributeTuple); - /* OK, capture the statistics object's existing definition string */ - char *defstring = pg_get_statisticsobjdef_string(stxoid); ++ AclResult aclresult; - /* Ignore if dropped or not inherited */ - if (att->attisdropped) - continue; - if (att->attinhcount <= 0) - continue; - tab->changedStatisticsOids = lappend_oid(tab->changedStatisticsOids, - stxoid); - tab->changedStatisticsDefs = lappend(tab->changedStatisticsDefs, - defstring); ++ aclresult = object_aclcheck(TableSpaceRelationId, tablespaceId, GetUserId(), ACL_CREATE); ++ if (aclresult != ACLCHECK_OK) ++ aclcheck_error(aclresult, OBJECT_TABLESPACE, tablespacename); + } + - 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); ++ /* Save info for Phase 3 to do the real work */ ++ if (OidIsValid(tab->newTableSpace)) ++ ereport(ERROR, ++ (errcode(ERRCODE_SYNTAX_ERROR), ++ errmsg("cannot have multiple SET TABLESPACE subcommands"))); + - copy_att->attinhcount--; - if (copy_att->attinhcount == 0) - copy_att->attislocal = true; ++ tab->newTableSpace = tablespaceId; + } - CatalogTupleUpdate(catalogRelation, ©Tuple->t_self, copyTuple); - heap_freetuple(copyTuple); - } - } - systable_endscan(scan); - table_close(catalogRelation, RowExclusiveLock); + /* - * Cleanup after we've finished all the ALTER TYPE or SET EXPRESSION - * operations for a particular relation. We have to drop and recreate all the - * indexes and constraints that depend on the altered columns. We do the - * actual dropping here, but re-creation is managed by adding work queue - * entries to do those steps later. ++ * Set, reset, or replace reloptions. + */ + static void -ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) ++ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation, ++ LOCKMODE lockmode) + { - ObjectAddress obj; - ObjectAddresses *objects; - ListCell *def_item; - ListCell *oid_item; ++ Oid relid; ++ Relation pgclass; ++ HeapTuple tuple; ++ HeapTuple newtuple; ++ Datum datum; ++ Datum newOptions; ++ Datum repl_val[Natts_pg_class]; ++ bool repl_null[Natts_pg_class]; ++ bool repl_repl[Natts_pg_class]; ++ const char *const validnsps[] = HEAP_RELOPT_NAMESPACES; -- /* - * 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. - * Collect all the constraints and indexes to drop so we can process them - * in a single call. That way we don't have to worry about dependencies - * among them. -- */ - attmap = build_attrmap_by_name(RelationGetDescr(child_rel), - RelationGetDescr(parent_rel), - false); - objects = new_object_addresses(); ++ if (defList == NIL && operation != AT_ReplaceRelOptions) ++ return; /* nothing to do */ - 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); - /* - * Re-parse the index and constraint definitions, and attach them to the - * appropriate work queue entries. We do this before dropping because in - * the case of a constraint on another table, we might not yet have - * exclusive lock on the table the constraint is attached to, and we need - * to get that before reparsing/dropping. (That's possible at least for - * FOREIGN KEY, CHECK, and EXCLUSION constraints; in non-FK cases it - * requires a dependency on the target table's composite type in the other - * table's constraint expressions.) - * - * We can't rely on the output of deparsing to tell us which relation to - * operate on, because concurrent activity might have made the name - * resolve differently. Instead, we've got to use the OID of the - * constraint or index we're processing to figure out which relation to - * operate on. - */ - forboth(oid_item, tab->changedConstraintOids, - def_item, tab->changedConstraintDefs) ++ pgclass = table_open(RelationRelationId, RowExclusiveLock); + - connames = NIL; - nncolumns = NIL; ++ /* Fetch heap tuple */ ++ relid = RelationGetRelid(rel); ++ tuple = SearchSysCacheLocked1(RELOID, ObjectIdGetDatum(relid)); ++ if (!HeapTupleIsValid(tuple)) ++ elog(ERROR, "cache lookup failed for relation %u", relid); + - while (HeapTupleIsValid(constraintTuple = systable_getnext(scan))) ++ if (operation == AT_ReplaceRelOptions) { - 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); - Oid oldId = lfirst_oid(oid_item); - HeapTuple tup; - Form_pg_constraint con; - Oid relid; - Oid confrelid; - bool conislocal; ++ /* ++ * If we're supposed to replace the reloptions list, we just pretend ++ * there were none before. ++ */ ++ datum = (Datum) 0; ++ } ++ else ++ { ++ bool isnull; - nncolumns = lappend_int(nncolumns, attmap->attnums[parent_attno - 1]); - tup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(oldId)); - if (!HeapTupleIsValid(tup)) /* should not happen */ - elog(ERROR, "cache lookup failed for constraint %u", oldId); - con = (Form_pg_constraint) GETSTRUCT(tup); - if (OidIsValid(con->conrelid)) - relid = con->conrelid; - else - { - /* must be a domain constraint */ - relid = get_typ_typrelid(getBaseType(con->contypid)); - if (!OidIsValid(relid)) - elog(ERROR, "could not identify relation associated with constraint %u", oldId); -- } - confrelid = con->confrelid; - conislocal = con->conislocal; - ReleaseSysCache(tup); ++ /* Get the old reloptions */ ++ datum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, ++ &isnull); ++ if (isnull) ++ datum = (Datum) 0; + } - systable_endscan(scan); - ObjectAddressSet(obj, ConstraintRelationId, oldId); - add_exact_object_address(&obj, objects); ++ /* Generate new proposed reloptions (text array) */ ++ newOptions = transformRelOptions(datum, defList, NULL, validnsps, false, ++ operation == AT_ResetRelOptions); + - /* 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); ++ /* Validate */ ++ switch (rel->rd_rel->relkind) ++ { ++ case RELKIND_RELATION: ++ case RELKIND_MATVIEW: ++ (void) heap_reloptions(rel->rd_rel->relkind, newOptions, true); ++ break; ++ case RELKIND_PARTITIONED_TABLE: ++ (void) partitioned_table_reloptions(newOptions, true); ++ break; ++ case RELKIND_VIEW: ++ (void) view_reloptions(newOptions, true); ++ break; ++ case RELKIND_INDEX: ++ case RELKIND_PARTITIONED_INDEX: ++ (void) index_reloptions(rel->rd_indam->amoptions, newOptions, true); ++ break; ++ case RELKIND_TOASTVALUE: ++ /* fall through to error -- shouldn't ever get here */ ++ default: ++ ereport(ERROR, ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("cannot set options for relation \"%s\"", ++ RelationGetRelationName(rel)), ++ errdetail_relkind_not_supported(rel->rd_rel->relkind))); ++ break; ++ } + - while (HeapTupleIsValid(constraintTuple = systable_getnext(scan))) ++ /* Special-case validation of view options */ ++ if (rel->rd_rel->relkind == RELKIND_VIEW) + { - Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(constraintTuple); - bool match = false; ++ Query *view_query = get_view_query(rel); ++ List *view_options = untransformRelOptions(newOptions); ++ ListCell *cell; ++ bool check_option = 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) ++ foreach(cell, view_options) + { - AttrNumber child_attno = extractNotNullColumn(constraintTuple); ++ DefElem *defel = (DefElem *) lfirst(cell); - foreach_int(prevattno, nncolumns) - { - if (prevattno == child_attno) - { - match = true; - nncolumns = foreach_delete_current(nncolumns, prevattno); - break; - } - } - /* - * If the constraint is inherited (only), we don't want to inject a - * new definition here; it'll get recreated when - * ATAddCheckNNConstraint recurses from adding the parent table's - * constraint. But we had to carry the info this far so that we can - * drop the constraint below. - */ - if (!conislocal) - continue; ++ if (strcmp(defel->defname, "check_option") == 0) ++ check_option = true; + } - 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; + /* - * When rebuilding another table's constraint that references the - * table we're modifying, we might not yet have any lock on the other - * table, so get one now. We'll need AccessExclusiveLock for the DROP - * CONSTRAINT step, so there's no value in asking for anything weaker. ++ * If the check option is specified, look to see if the view is ++ * actually auto-updatable or not. + */ - if (relid != tab->relid) - LockRelationOid(relid, AccessExclusiveLock); ++ if (check_option) ++ { ++ const char *view_updatable_error = ++ view_query_is_auto_updatable(view_query, true); - CatalogTupleUpdate(catalogRelation, ©Tuple->t_self, copyTuple); - heap_freetuple(copyTuple); - ATPostAlterTypeParse(oldId, relid, confrelid, - (char *) lfirst(def_item), - wqueue, lockmode, tab->rewrite); ++ if (view_updatable_error) ++ ereport(ERROR, ++ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ++ errmsg("WITH CHECK OPTION is supported only on automatically updatable views"), ++ errhint("%s", _(view_updatable_error)))); + } } - forboth(oid_item, tab->changedIndexOids, - def_item, tab->changedIndexDefs) - { - Oid oldId = lfirst_oid(oid_item); - Oid relid; - /* 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)); - - relid = IndexGetRelation(oldId, false); + /* - * 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. ++ * All we need do here is update the pg_class row; the new options will be ++ * propagated into relcaches during post-commit cache inval. + */ - InvokeObjectPostAlterHookArg(InheritsRelationId, - RelationGetRelid(child_rel), 0, - RelationGetRelid(parent_rel), false); - } ++ memset(repl_val, 0, sizeof(repl_val)); ++ memset(repl_null, false, sizeof(repl_null)); ++ memset(repl_repl, false, sizeof(repl_repl)); - /* - * 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; - /* - * As above, make sure we have lock on the index's table if it's not - * the same table. - */ - if (relid != tab->relid) - LockRelationOid(relid, AccessExclusiveLock); ++ if (newOptions != (Datum) 0) ++ repl_val[Anum_pg_class_reloptions - 1] = newOptions; ++ else ++ repl_null[Anum_pg_class_reloptions - 1] = true; - catalogRelation = table_open(DependRelationId, RowExclusiveLock); - ATPostAlterTypeParse(oldId, relid, InvalidOid, - (char *) lfirst(def_item), - wqueue, lockmode, tab->rewrite); ++ repl_repl[Anum_pg_class_reloptions - 1] = true; - 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)); - ObjectAddressSet(obj, RelationRelationId, oldId); - add_exact_object_address(&obj, objects); - } ++ newtuple = heap_modify_tuple(tuple, RelationGetDescr(pgclass), ++ repl_val, repl_null, repl_repl); - scan = systable_beginscan(catalogRelation, DependDependerIndexId, true, - NULL, 3, key); - /* add dependencies for new statistics */ - forboth(oid_item, tab->changedStatisticsOids, - def_item, tab->changedStatisticsDefs) ++ CatalogTupleUpdate(pgclass, &newtuple->t_self, newtuple); ++ UnlockTuple(pgclass, &tuple->t_self, InplaceUpdateTupleLock); + - while (HeapTupleIsValid(depTuple = systable_getnext(scan))) - { - Form_pg_depend dep = (Form_pg_depend) GETSTRUCT(depTuple); ++ InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0); + - if (dep->refclassid == refclassid && - dep->refobjid == refobjid && - dep->refobjsubid == 0 && - dep->deptype == deptype) - CatalogTupleDelete(catalogRelation, &depTuple->t_self); - } ++ heap_freetuple(newtuple); + - systable_endscan(scan); - table_close(catalogRelation, RowExclusiveLock); - } ++ ReleaseSysCache(tuple); + - /* - * 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; ++ /* repeat the whole exercise for the toast table, if there's one */ ++ if (OidIsValid(rel->rd_rel->reltoastrelid)) + { - Oid oldId = lfirst_oid(oid_item); - Oid relid; ++ Relation toastrel; ++ Oid toastid = rel->rd_rel->reltoastrelid; - /* Validate the type. */ - typetuple = typenameType(NULL, ofTypename, NULL); - check_of_type(typetuple); - typeform = (Form_pg_type) GETSTRUCT(typetuple); - typeid = typeform->oid; - relid = StatisticsGetRelation(oldId, false); ++ toastrel = table_open(toastid, lockmode); - /* 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); - /* - * As above, make sure we have lock on the statistics object's table - * if it's not the same table. However, we take - * ShareUpdateExclusiveLock here, aligning with the lock level used in - * CreateStatistics and RemoveStatisticsById. - * - * CAUTION: this should be done after all cases that grab - * AccessExclusiveLock, else we risk causing deadlock due to needing - * to promote our table lock. - */ - if (relid != tab->relid) - LockRelationOid(relid, ShareUpdateExclusiveLock); ++ /* Fetch heap tuple */ ++ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(toastid)); ++ if (!HeapTupleIsValid(tuple)) ++ elog(ERROR, "cache lookup failed for relation %u", toastid); - /* - * 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; - ATPostAlterTypeParse(oldId, relid, InvalidOid, - (char *) lfirst(def_item), - wqueue, lockmode, tab->rewrite); ++ if (operation == AT_ReplaceRelOptions) ++ { ++ /* ++ * If we're supposed to replace the reloptions list, we just ++ * pretend there were none before. ++ */ ++ datum = (Datum) 0; ++ } ++ else ++ { ++ bool isnull; - /* Get the next non-dropped type attribute. */ - type_attr = TupleDescAttr(typeTupleDesc, type_attno - 1); - if (type_attr->attisdropped) - continue; - type_attname = NameStr(type_attr->attname); - ObjectAddressSet(obj, StatisticExtRelationId, oldId); - add_exact_object_address(&obj, objects); - } ++ /* Get the old reloptions */ ++ datum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, ++ &isnull); ++ if (isnull) ++ datum = (Datum) 0; ++ } - /* 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); - /* - * Queue up command to restore replica identity index marking - */ - if (tab->replicaIdentityIndex) - { - AlterTableCmd *cmd = makeNode(AlterTableCmd); - ReplicaIdentityStmt *subcmd = makeNode(ReplicaIdentityStmt); ++ newOptions = transformRelOptions(datum, defList, "toast", validnsps, ++ false, operation == AT_ResetRelOptions); - /* 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))); - subcmd->identity_type = REPLICA_IDENTITY_INDEX; - subcmd->name = tab->replicaIdentityIndex; - cmd->subtype = AT_ReplicaIdentity; - cmd->def = (Node *) subcmd; ++ (void) heap_reloptions(RELKIND_TOASTVALUE, newOptions, true); - /* 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))); - /* do it after indexes and constraints */ - tab->subcmds[AT_PASS_OLD_CONSTR] = - lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd); -- } - ReleaseTupleDesc(typeTupleDesc); ++ memset(repl_val, 0, sizeof(repl_val)); ++ memset(repl_null, false, sizeof(repl_null)); ++ memset(repl_repl, false, sizeof(repl_repl)); - /* Any remaining columns at the end of the table had better be dropped. */ - for (; table_attno <= tableTupleDesc->natts; table_attno++) - /* - * Queue up command to restore marking of index used for cluster. - */ - if (tab->clusterOnIndex) -- { - Form_pg_attribute table_attr = TupleDescAttr(tableTupleDesc, - table_attno - 1); - AlterTableCmd *cmd = makeNode(AlterTableCmd); ++ if (newOptions != (Datum) 0) ++ repl_val[Anum_pg_class_reloptions - 1] = newOptions; ++ else ++ repl_null[Anum_pg_class_reloptions - 1] = true; - if (!table_attr->attisdropped) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("table has extra column \"%s\"", - NameStr(table_attr->attname)))); - } - cmd->subtype = AT_ClusterOn; - cmd->name = tab->clusterOnIndex; ++ repl_repl[Anum_pg_class_reloptions - 1] = true; - /* 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); - /* do it after indexes and constraints */ - tab->subcmds[AT_PASS_OLD_CONSTR] = - lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd); - } ++ newtuple = heap_modify_tuple(tuple, RelationGetDescr(pgclass), ++ repl_val, repl_null, repl_repl); - /* 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); - /* - * It should be okay to use DROP_RESTRICT here, since nothing else should - * be depending on these objects. - */ - performMultipleDeletions(objects, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); ++ CatalogTupleUpdate(pgclass, &newtuple->t_self, newtuple); - /* 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); - free_object_addresses(objects); ++ InvokeObjectPostAlterHookArg(RelationRelationId, ++ RelationGetRelid(toastrel), 0, ++ InvalidOid, true); - InvokeObjectPostAlterHook(RelationRelationId, relid, 0); - /* - * The objects will get recreated during subsequent passes over the work - * queue. - */ ++ heap_freetuple(newtuple); + - heap_freetuple(classtuple); - table_close(relationRelation, RowExclusiveLock); ++ ReleaseSysCache(tuple); + - ReleaseSysCache(typetuple); ++ table_close(toastrel, NoLock); ++ } + - return typeobj; ++ table_close(pgclass, RowExclusiveLock); } /* - * ALTER TABLE NOT OF - * Parse the previously-saved definition string for a constraint, index or - * statistics object against the newly-established column data type(s), and - * queue up the resulting command parsetrees for execution. -- * - * Detach a typed table from its originating type. Just clear reloftype and - * remove the dependency. - * This might fail if, for example, you have a WHERE clause that uses an - * operator that's not available for the new column type. ++ * 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 - ATExecDropOf(Relation rel, LOCKMODE lockmode) -ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd, - List **wqueue, LOCKMODE lockmode, bool rewrite) ++ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode) { - Oid relid = RelationGetRelid(rel); - Relation relationRelation; - HeapTuple tuple; - List *raw_parsetree_list; - List *querytree_list; - ListCell *list_item; + Relation rel; ++ Oid reltoastrelid; ++ RelFileNumber newrelfilenumber; ++ RelFileLocator newrlocator; ++ List *reltoastidxids = NIL; ++ ListCell *lc; - if (!OidIsValid(rel->rd_rel->reloftype)) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is not a typed table", - RelationGetRelationName(rel)))); + /* - * We expect that we will get only ALTER TABLE and CREATE INDEX - * statements. Hence, there is no need to pass them through - * parse_analyze_*() or the rewriter, but instead we need to pass them - * through parse_utilcmd.c to make them ready for execution. ++ * Need lock here in case we are recursing to toast table or index + */ - raw_parsetree_list = raw_parser(cmd, RAW_PARSE_DEFAULT); - querytree_list = NIL; - foreach(list_item, raw_parsetree_list) ++ rel = relation_open(tableOid, lockmode); ++ ++ /* Check first if relation can be moved to new tablespace */ ++ if (!CheckRelationTableSpaceMove(rel, newTableSpace)) + { - RawStmt *rs = lfirst_node(RawStmt, list_item); - Node *stmt = rs->stmt; ++ InvokeObjectPostAlterHook(RelationRelationId, ++ RelationGetRelid(rel), 0); ++ relation_close(rel, NoLock); ++ return; ++ } + - if (IsA(stmt, IndexStmt)) - querytree_list = lappend(querytree_list, - transformIndexStmt(oldRelId, - (IndexStmt *) stmt, - cmd)); - else if (IsA(stmt, AlterTableStmt)) - { - List *beforeStmts; - List *afterStmts; ++ reltoastrelid = rel->rd_rel->reltoastrelid; ++ /* Fetch the list of indexes on toast relation if necessary */ ++ if (OidIsValid(reltoastrelid)) ++ { ++ Relation toastRel = relation_open(reltoastrelid, lockmode); + - stmt = (Node *) transformAlterTableStmt(oldRelId, - (AlterTableStmt *) stmt, - cmd, - &beforeStmts, - &afterStmts); - querytree_list = list_concat(querytree_list, beforeStmts); - querytree_list = lappend(querytree_list, stmt); - querytree_list = list_concat(querytree_list, afterStmts); - } - else if (IsA(stmt, CreateStatsStmt)) - querytree_list = lappend(querytree_list, - transformStatsStmt(oldRelId, - (CreateStatsStmt *) stmt, - cmd)); - else - querytree_list = lappend(querytree_list, stmt); ++ reltoastidxids = RelationGetIndexList(toastRel); ++ relation_close(toastRel, lockmode); + } - /* Caller should already have acquired whatever lock we need. */ - rel = relation_open(oldRelId, NoLock); - /* - * 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. - * Attach each generated command to the proper place in the work queue. - * Note this could result in creation of entirely new work-queue entries. - * - * Also note that we have to tweak the command subtypes, because it turns - * out that re-creation of indexes and constraints has to act a bit - * differently from initial creation. ++ * Relfilenumbers are not unique in databases across tablespaces, so we ++ * need to allocate a new one in the new tablespace. + */ - foreach(list_item, querytree_list) - { - Node *stm = (Node *) lfirst(list_item); - AlteredTableInfo *tab; - - tab = ATGetQueueEntry(wqueue, rel); - - if (IsA(stm, IndexStmt)) - { - IndexStmt *stmt = (IndexStmt *) stm; - AlterTableCmd *newcmd; - - if (!rewrite) - TryReuseIndex(oldId, stmt); - stmt->reset_default_tblspc = true; - /* keep the index's comment */ - stmt->idxcomment = GetComment(oldId, RelationRelationId, 0); - - newcmd = makeNode(AlterTableCmd); - newcmd->subtype = AT_ReAddIndex; - newcmd->def = (Node *) stmt; - tab->subcmds[AT_PASS_OLD_INDEX] = - lappend(tab->subcmds[AT_PASS_OLD_INDEX], newcmd); - } - else if (IsA(stm, AlterTableStmt)) - { - AlterTableStmt *stmt = (AlterTableStmt *) stm; - ListCell *lcmd; ++ newrelfilenumber = GetNewRelFileNumber(newTableSpace, NULL, ++ rel->rd_rel->relpersistence); + - foreach(lcmd, stmt->cmds) - { - AlterTableCmd *cmd = lfirst_node(AlterTableCmd, lcmd); ++ /* Open old and new relation */ ++ newrlocator = rel->rd_locator; ++ newrlocator.relNumber = newrelfilenumber; ++ newrlocator.spcOid = newTableSpace; + - if (cmd->subtype == AT_AddIndex) - { - IndexStmt *indstmt; - Oid indoid; ++ /* 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); ++ } + - indstmt = castNode(IndexStmt, cmd->def); - indoid = get_constraint_index(oldId); ++ /* ++ * Update the pg_class row. ++ * ++ * NB: This wouldn't work if ATExecSetTableSpace() were allowed to be ++ * executed on pg_class or its indexes (the above copy wouldn't contain ++ * the updated pg_class entry), but that's forbidden with ++ * CheckRelationTableSpaceMove(). + */ ++ SetRelationTableSpace(rel, newTableSpace, newrelfilenumber); - drop_parent_dependency(relid, TypeRelationId, rel->rd_rel->reloftype, - DEPENDENCY_NORMAL); - if (!rewrite) - TryReuseIndex(indoid, indstmt); - /* keep any comment on the index */ - indstmt->idxcomment = GetComment(indoid, - RelationRelationId, 0); - indstmt->reset_default_tblspc = true; ++ InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0); - /* 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); - cmd->subtype = AT_ReAddIndex; - tab->subcmds[AT_PASS_OLD_INDEX] = - lappend(tab->subcmds[AT_PASS_OLD_INDEX], cmd); ++ RelationAssumeNewRelfilelocator(rel); - InvokeObjectPostAlterHook(RelationRelationId, relid, 0); - /* recreate any comment on the constraint */ - RebuildConstraintComment(tab, - AT_PASS_OLD_INDEX, - oldId, - rel, - NIL, - indstmt->idxname); - } - else if (cmd->subtype == AT_AddConstraint) - { - Constraint *con = castNode(Constraint, cmd->def); ++ relation_close(rel, NoLock); - heap_freetuple(tuple); - table_close(relationRelation, RowExclusiveLock); - con->old_pktable_oid = refRelId; - /* rewriting neither side of a FK */ - if (con->contype == CONSTR_FOREIGN && - !rewrite && tab->rewrite == 0) - TryReuseForeignKey(oldId, con); - con->reset_default_tblspc = true; - cmd->subtype = AT_ReAddConstraint; - tab->subcmds[AT_PASS_OLD_CONSTR] = - lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd); ++ /* Make sure the reltablespace change is visible */ ++ CommandCounterIncrement(); + - /* - * Recreate any comment on the constraint. If we have - * recreated a primary key, then transformTableConstraint - * has added an unnamed not-null constraint here; skip - * this in that case. - */ - if (con->conname) - RebuildConstraintComment(tab, - AT_PASS_OLD_CONSTR, - oldId, - rel, - NIL, - con->conname); - else - Assert(con->contype == CONSTR_NOTNULL); - } - else - elog(ERROR, "unexpected statement subtype: %d", - (int) cmd->subtype); - } - } - else if (IsA(stm, AlterDomainStmt)) - { - AlterDomainStmt *stmt = (AlterDomainStmt *) stm; ++ /* Move associated toast relation and/or indexes, too */ ++ if (OidIsValid(reltoastrelid)) ++ ATExecSetTableSpace(reltoastrelid, newTableSpace, lockmode); ++ foreach(lc, reltoastidxids) ++ ATExecSetTableSpace(lfirst_oid(lc), newTableSpace, lockmode); + - if (stmt->subtype == AD_AddConstraint) - { - Constraint *con = castNode(Constraint, stmt->def); - AlterTableCmd *cmd = makeNode(AlterTableCmd); ++ /* Clean up */ ++ list_free(reltoastidxids); +} - cmd->subtype = AT_ReAddDomainConstraint; - cmd->def = (Node *) stmt; - tab->subcmds[AT_PASS_OLD_CONSTR] = - lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd); +/* - * 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. ++ * Special handling of ALTER TABLE SET TABLESPACE for relations with no ++ * storage that have an interest in preserving tablespace. + * - * Caller had better hold an exclusive lock on the relation, as the results - * of running two of these concurrently wouldn't be pretty. ++ * Since these have no storage the tablespace can be updated with a simple ++ * metadata only operation to update the tablespace. + */ +static void - relation_mark_replica_identity(Relation rel, char ri_type, Oid indexOid, - bool is_internal) ++ATExecSetTableSpaceNoStorage(Relation rel, Oid newTableSpace) +{ - 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. ++ * Shouldn't be called on relations having storage; these are processed in ++ * phase 3. + */ - 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); ++ Assert(!RELKIND_HAS_STORAGE(rel->rd_rel->relkind)); - /* - * Update the per-index indisreplident flags correctly. - */ - pg_index = table_open(IndexRelationId, RowExclusiveLock); - foreach(index, RelationGetIndexList(rel)) - /* recreate any comment on the constraint */ - RebuildConstraintComment(tab, - AT_PASS_OLD_CONSTR, - oldId, - NULL, - stmt->typeName, - con->conname); - } - else - elog(ERROR, "unexpected statement subtype: %d", - (int) stmt->subtype); - } - else if (IsA(stm, CreateStatsStmt)) - { - CreateStatsStmt *stmt = (CreateStatsStmt *) stm; - AlterTableCmd *newcmd; ++ /* check if relation can be moved to its new tablespace */ ++ if (!CheckRelationTableSpaceMove(rel, newTableSpace)) + { - 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; - } - } ++ InvokeObjectPostAlterHook(RelationRelationId, ++ RelationGetRelid(rel), ++ 0); ++ return; ++ } - if (dirty) - { - CatalogTupleUpdate(pg_index, &pg_index_tuple->t_self, pg_index_tuple); - InvokeObjectPostAlterHookArg(IndexRelationId, thisIndexOid, 0, - InvalidOid, is_internal); - /* keep the statistics object's comment */ - stmt->stxcomment = GetComment(oldId, StatisticExtRelationId, 0); ++ /* Update can be done, so change reltablespace */ ++ SetRelationTableSpace(rel, newTableSpace, InvalidOid); - /* - * 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); - newcmd = makeNode(AlterTableCmd); - newcmd->subtype = AT_ReAddStatistics; - newcmd->def = (Node *) stmt; - tab->subcmds[AT_PASS_MISC] = - lappend(tab->subcmds[AT_PASS_MISC], newcmd); -- } - heap_freetuple(pg_index_tuple); - else - elog(ERROR, "unexpected statement type: %d", - (int) nodeTag(stm)); -- } ++ InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0); - table_close(pg_index, RowExclusiveLock); - relation_close(rel, NoLock); ++ /* Make sure the reltablespace change is visible */ ++ CommandCounterIncrement(); } /* - * ALTER TABLE REPLICA IDENTITY ... - * Subroutine for ATPostAlterTypeParse() to recreate any existing comment - * for a table or domain constraint that is being rebuilt. ++ * Alter Table ALL ... SET TABLESPACE + * - * objid is the OID of the constraint. - * Pass "rel" for a table constraint, or "domname" (domain's qualified name - * as a string list) for a domain constraint. - * (We could dig that info, as well as the conname, out of the pg_constraint - * entry; but callers already have them so might as well pass them.) ++ * 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). */ --static void - ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode) -RebuildConstraintComment(AlteredTableInfo *tab, AlterTablePass pass, Oid objid, - Relation rel, List *domname, - const char *conname) ++Oid ++AlterTableMoveAll(AlterTableMoveAllStmt *stmt) { - Oid indexOid; - Relation indexRel; - int key; - CommentStmt *cmd; - char *comment_str; - AlterTableCmd *newcmd; ++ 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); + - /* Look for comment for object wanted, and leave if none */ - comment_str = GetComment(objid, ConstraintRelationId, 0); - if (comment_str == NULL) - return; ++ /* Ensure we were not asked to move something we can't */ ++ if (stmt->objtype != OBJECT_TABLE && stmt->objtype != OBJECT_INDEX && ++ stmt->objtype != OBJECT_MATVIEW) ++ ereport(ERROR, ++ (errcode(ERRCODE_INVALID_PARAMETER_VALUE), ++ errmsg("only tables, indexes, and materialized views exist in tablespaces"))); ++ ++ /* Get the orig and new tablespace OIDs */ ++ orig_tablespaceoid = get_tablespace_oid(stmt->orig_tablespacename, false); ++ new_tablespaceoid = get_tablespace_oid(stmt->new_tablespacename, false); ++ ++ /* Can't move shared relations in to or out of pg_global */ ++ /* This is also checked by ATExecSetTableSpace, but nice to stop earlier */ ++ if (orig_tablespaceoid == GLOBALTABLESPACE_OID || ++ new_tablespaceoid == GLOBALTABLESPACE_OID) ++ ereport(ERROR, ++ (errcode(ERRCODE_INVALID_PARAMETER_VALUE), ++ errmsg("cannot move relations in to or out of pg_global tablespace"))); - 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) - /* Build CommentStmt node, copying all input data for safety */ - cmd = makeNode(CommentStmt); - if (rel) -- { - relation_mark_replica_identity(rel, stmt->identity_type, InvalidOid, true); - return; - cmd->objtype = OBJECT_TABCONSTRAINT; - cmd->object = (Node *) - list_make3(makeString(get_namespace_name(RelationGetNamespace(rel))), - makeString(pstrdup(RelationGetRelationName(rel))), - makeString(pstrdup(conname))); -- } - else if (stmt->identity_type == REPLICA_IDENTITY_INDEX) - else ++ /* ++ * 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) { - /* fallthrough */ ; - cmd->objtype = OBJECT_DOMCONSTRAINT; - cmd->object = (Node *) - list_make2(makeTypeNameFromNameList(copyObject(domname)), - makeString(pstrdup(conname))); ++ AclResult aclresult; ++ ++ aclresult = object_aclcheck(TableSpaceRelationId, new_tablespaceoid, GetUserId(), ++ ACL_CREATE); ++ if (aclresult != ACLCHECK_OK) ++ aclcheck_error(aclresult, OBJECT_TABLESPACE, ++ get_tablespace_name(new_tablespaceoid)); } - else - elog(ERROR, "unexpected identity type %u", stmt->identity_type); - cmd->comment = comment_str; - /* 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)))); - /* Append it to list of commands */ - newcmd = makeNode(AlterTableCmd); - newcmd->subtype = AT_ReAddComment; - newcmd->def = (Node *) cmd; - tab->subcmds[pass] = lappend(tab->subcmds[pass], newcmd); -} ++ /* ++ * 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; - indexRel = index_open(indexOid, ShareLock); -/* - * Subroutine for ATPostAlterTypeParse(). Calls out to CheckIndexCompatible() - * for the real analysis, then mutates the IndexStmt based on that verdict. - */ -static void -TryReuseIndex(Oid oldId, IndexStmt *stmt) -{ - if (CheckIndexCompatible(oldId, - stmt->accessMethod, - stmt->indexParams, - stmt->excludeOpNames, - stmt->iswithoutoverlaps)) ++ if (new_tablespaceoid == MyDatabaseTableSpace) ++ new_tablespaceoid = InvalidOid; + - /* 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)))); ++ /* no-op */ ++ if (orig_tablespaceoid == new_tablespaceoid) ++ return new_tablespaceoid; + + /* - * 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. ++ * Walk the list of objects in the tablespace and move them. This will ++ * only find objects in our database, of course. + */ - 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)))); ++ ScanKeyInit(&key[0], ++ Anum_pg_class_reltablespace, ++ BTEqualStrategyNumber, F_OIDEQ, ++ ObjectIdGetDatum(orig_tablespaceoid)); + - /* Check index for nullable columns. */ - for (key = 0; key < IndexRelationGetNumberOfKeyAttributes(indexRel); key++) ++ rel = table_open(RelationRelationId, AccessShareLock); ++ scan = table_beginscan_catalog(rel, 1, key); ++ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) { - int16 attno = indexRel->rd_index->indkey.values[key]; - Form_pg_attribute attr; - Relation irel = index_open(oldId, NoLock); ++ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple); ++ Oid relOid = relForm->oid; - /* If it's a partitioned index, there is no storage to share. */ - if (irel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX) - { - stmt->oldNumber = irel->rd_locator.relNumber; - stmt->oldCreateSubid = irel->rd_createSubid; - stmt->oldFirstRelfilelocatorSubid = irel->rd_firstRelfilelocatorSubid; - } - index_close(irel, NoLock); - } -} + /* - * 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.) ++ * 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 (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))); ++ if (IsCatalogNamespace(relForm->relnamespace) || ++ relForm->relisshared || ++ isAnyTempNamespace(relForm->relnamespace) || ++ IsToastNamespace(relForm->relnamespace)) ++ continue; - attr = TupleDescAttr(rel->rd_att, attno - 1); - if (!attr->attnotnull) -/* - * Subroutine for ATPostAlterTypeParse(). - * - * Stash the old P-F equality operator into the Constraint node, for possible - * use by ATAddForeignKeyConstraint() in determining whether revalidation of - * this constraint can be skipped. - */ -static void -TryReuseForeignKey(Oid oldId, Constraint *con) -{ - HeapTuple tup; - Datum adatum; - ArrayType *arr; - Oid *rawarr; - int numkeys; - int i; ++ /* 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; + - Assert(con->contype == CONSTR_FOREIGN); - Assert(con->old_conpfeqop == NIL); /* already prepared this node */ ++ /* Check if we are only moving objects owned by certain roles */ ++ if (role_oids != NIL && !list_member_oid(role_oids, relForm->relowner)) ++ continue; + - tup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(oldId)); - if (!HeapTupleIsValid(tup)) /* should not happen */ - elog(ERROR, "cache lookup failed for constraint %u", oldId); ++ /* ++ * Handle permissions-checking here since we are locking the tables ++ * and also to avoid doing a bunch of work only to fail part-way. Note ++ * that permissions will also be checked by AlterTableInternal(). ++ * ++ * Caller must be considered an owner on the table to move it. ++ */ ++ if (!object_ownercheck(RelationRelationId, relOid, GetUserId())) ++ aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relOid)), ++ NameStr(relForm->relname)); + - adatum = SysCacheGetAttrNotNull(CONSTROID, tup, - Anum_pg_constraint_conpfeqop); - arr = DatumGetArrayTypeP(adatum); /* ensure not toasted */ - numkeys = ARR_DIMS(arr)[0]; - /* test follows the one in ri_FetchConstraintInfo() */ - if (ARR_NDIM(arr) != 1 || - ARR_HASNULL(arr) || - ARR_ELEMTYPE(arr) != OIDOID) - elog(ERROR, "conpfeqop is not a 1-D Oid array"); - rawarr = (Oid *) ARR_DATA_PTR(arr); ++ if (stmt->nowait && ++ !ConditionalLockRelationOid(relOid, AccessExclusiveLock)) + 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)))); ++ (errcode(ERRCODE_OBJECT_IN_USE), ++ errmsg("aborting because lock on relation \"%s.%s\" is not available", ++ get_namespace_name(relForm->relnamespace), ++ NameStr(relForm->relname)))); ++ else ++ LockRelationOid(relOid, AccessExclusiveLock); + - /* stash a List of the operator Oids in our Constraint node */ - for (i = 0; i < numkeys; i++) - con->old_conpfeqop = lappend_oid(con->old_conpfeqop, rawarr[i]); ++ /* Add to our list of objects to move */ ++ relations = lappend_oid(relations, relOid); + } - /* This index is suitable for use as a replica identity. Mark it. */ - relation_mark_replica_identity(rel, stmt->identity_type, indexOid, true); - ReleaseSysCache(tup); -} ++ table_endscan(scan); ++ table_close(rel, AccessShareLock); - index_close(indexRel, NoLock); -/* - * ALTER COLUMN .. OPTIONS ( ... ) - * - * Returns the address of the modified column - */ -static ObjectAddress -ATExecAlterColumnGenericOptions(Relation rel, - const char *colName, - List *options, - LOCKMODE lockmode) -{ - Relation ftrel; - Relation attrel; - ForeignServer *server; - ForeignDataWrapper *fdw; - HeapTuple tuple; - HeapTuple newtuple; - bool isnull; - Datum repl_val[Natts_pg_attribute]; - bool repl_null[Natts_pg_attribute]; - bool repl_repl[Natts_pg_attribute]; - Datum datum; - Form_pg_foreign_table fttableform; - Form_pg_attribute atttableform; - AttrNumber attnum; - ObjectAddress address; ++ if (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)))); + - if (options == NIL) - return InvalidObjectAddress; ++ /* Everything is locked, loop through and move all of the relations. */ ++ foreach(l, relations) ++ { ++ List *cmds = NIL; ++ AlterTableCmd *cmd = makeNode(AlterTableCmd); + - /* First, determine FDW validator associated to the foreign table. */ - ftrel = table_open(ForeignTableRelationId, AccessShareLock); - tuple = SearchSysCache1(FOREIGNTABLEREL, ObjectIdGetDatum(rel->rd_id)); - if (!HeapTupleIsValid(tuple)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("foreign table \"%s\" does not exist", - RelationGetRelationName(rel)))); - fttableform = (Form_pg_foreign_table) GETSTRUCT(tuple); - server = GetForeignServer(fttableform->ftserver); - fdw = GetForeignDataWrapper(server->fdwid); ++ cmd->subtype = AT_SetTableSpace; ++ cmd->name = stmt->new_tablespacename; + - table_close(ftrel, AccessShareLock); - ReleaseSysCache(tuple); ++ cmds = lappend(cmds, cmd); + - attrel = table_open(AttributeRelationId, RowExclusiveLock); - tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName); - if (!HeapTupleIsValid(tuple)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("column \"%s\" of relation \"%s\" does not exist", - colName, RelationGetRelationName(rel)))); ++ EventTriggerAlterTableStart((Node *) stmt); ++ /* OID is set by AlterTableInternal */ ++ AlterTableInternal(lfirst_oid(l), cmds, false); ++ EventTriggerAlterTableEnd(); ++ } + - /* Prevent them from altering a system attribute */ - atttableform = (Form_pg_attribute) GETSTRUCT(tuple); - attnum = atttableform->attnum; - if (attnum <= 0) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot alter system column \"%s\"", colName))); ++ return new_tablespaceoid; +} + - /* - * ALTER TABLE ENABLE/DISABLE ROW LEVEL SECURITY - */ +static void - ATExecSetRowSecurity(Relation rel, bool rls) ++index_copy_data(Relation rel, RelFileLocator newrlocator) +{ - Relation pg_class; - Oid relid; - HeapTuple tuple; ++ SMgrRelation dstrel; - relid = RelationGetRelid(rel); ++ /* ++ * Since we copy the file directly without looking at the shared buffers, ++ * we'd better first flush out any pages of the source relation that are ++ * in shared buffers. We assume no new changes will be made while we are ++ * holding exclusive lock on the rel. ++ */ ++ FlushRelationBuffers(rel); - /* Pull the record for this relation and update it */ - pg_class = table_open(RelationRelationId, RowExclusiveLock); - /* Initialize buffers for new tuple values */ - memset(repl_val, 0, sizeof(repl_val)); - memset(repl_null, false, sizeof(repl_null)); - memset(repl_repl, false, sizeof(repl_repl)); ++ /* ++ * Create and copy all forks of the relation, and schedule unlinking of ++ * old physical files. ++ * ++ * NOTE: any conflict in relfilenumber value will be caught in ++ * RelationCreateStorage(). ++ */ ++ dstrel = RelationCreateStorage(newrlocator, rel->rd_rel->relpersistence, true); - tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid)); - /* Extract the current options */ - datum = SysCacheGetAttr(ATTNAME, - tuple, - Anum_pg_attribute_attfdwoptions, - &isnull); - if (isnull) - datum = PointerGetDatum(NULL); ++ /* copy main fork */ ++ RelationCopyStorage(RelationGetSmgr(rel), dstrel, MAIN_FORKNUM, ++ rel->rd_rel->relpersistence); - if (!HeapTupleIsValid(tuple)) - elog(ERROR, "cache lookup failed for relation %u", relid); - /* Transform the options */ - datum = transformGenericOptions(AttributeRelationId, - datum, - options, - fdw->fdwvalidator); ++ /* copy those extra forks that exist */ ++ for (ForkNumber forkNum = MAIN_FORKNUM + 1; ++ forkNum <= MAX_FORKNUM; forkNum++) ++ { ++ if (smgrexists(RelationGetSmgr(rel), forkNum)) ++ { ++ smgrcreate(dstrel, forkNum, false); + - if (DatumGetPointer(datum) != NULL) - repl_val[Anum_pg_attribute_attfdwoptions - 1] = datum; - else - repl_null[Anum_pg_attribute_attfdwoptions - 1] = true; ++ /* ++ * WAL log creation if the relation is persistent, or this is the ++ * init fork of an unlogged relation. ++ */ ++ if (RelationIsPermanent(rel) || ++ (rel->rd_rel->relpersistence == RELPERSISTENCE_UNLOGGED && ++ forkNum == INIT_FORKNUM)) ++ log_smgrcreate(&newrlocator, forkNum); ++ RelationCopyStorage(RelationGetSmgr(rel), dstrel, forkNum, ++ rel->rd_rel->relpersistence); ++ } ++ } + - repl_repl[Anum_pg_attribute_attfdwoptions - 1] = true; ++ /* drop old relation, and close new one */ ++ RelationDropStorage(rel); ++ smgrclose(dstrel); ++} - ((Form_pg_class) GETSTRUCT(tuple))->relrowsecurity = rls; - CatalogTupleUpdate(pg_class, &tuple->t_self, tuple); - /* Everything looks good - update the tuple */ ++/* ++ * ALTER TABLE ENABLE/DISABLE TRIGGER ++ * ++ * We just pass this off to trigger.c. ++ */ ++static void ++ATExecEnableDisableTrigger(Relation rel, const char *trigname, ++ char fires_when, bool skip_system, bool recurse, ++ LOCKMODE lockmode) ++{ ++ EnableDisableTrigger(rel, trigname, InvalidOid, ++ fires_when, skip_system, recurse, ++ lockmode); - newtuple = heap_modify_tuple(tuple, RelationGetDescr(attrel), - repl_val, repl_null, repl_repl); + InvokeObjectPostAlterHook(RelationRelationId, + RelationGetRelid(rel), 0); - - table_close(pg_class, RowExclusiveLock); - heap_freetuple(tuple); +} - CatalogTupleUpdate(attrel, &newtuple->t_self, newtuple); +/* - * ALTER TABLE FORCE/NO FORCE ROW LEVEL SECURITY ++ * ALTER TABLE ENABLE/DISABLE RULE ++ * ++ * We just pass this off to rewriteDefine.c. + */ +static void - ATExecForceNoForceRowSecurity(Relation rel, bool force_rls) ++ATExecEnableDisableRule(Relation rel, const char *rulename, ++ char fires_when, LOCKMODE lockmode) +{ - 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); ++ EnableDisableRule(rel, rulename, fires_when); InvokeObjectPostAlterHook(RelationRelationId, - RelationGetRelid(rel), - atttableform->attnum); - ObjectAddressSubSet(address, RelationRelationId, - RelationGetRelid(rel), attnum); - - ReleaseSysCache(tuple); + RelationGetRelid(rel), 0); - - table_close(pg_class, RowExclusiveLock); - heap_freetuple(tuple); +} - table_close(attrel, RowExclusiveLock); +/* - * ALTER FOREIGN TABLE OPTIONS (...) ++ * ALTER TABLE INHERIT ++ * ++ * 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 void - ATExecGenericOptions(Relation rel, List *options) ++ATPrepAddInherit(Relation child_rel) +{ - 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)) ++ if (child_rel->rd_rel->reloftype) + 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); ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("cannot change inheritance of typed table"))); - table_close(ftrel, RowExclusiveLock); - heap_freetuple(newtuple); ++ if (child_rel->rd_rel->relispartition) ++ ereport(ERROR, ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("cannot change inheritance of a partition"))); - heap_freetuple(tuple); - return address; ++ if (child_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) ++ ereport(ERROR, ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("cannot change inheritance of partitioned table"))); } /* - * ALTER TABLE ALTER COLUMN SET COMPRESSION - * ALTER TABLE OWNER -- * - * Return value is the address of the modified column - * recursing is true if we are recursing from a table to its indexes, - * sequences, or toast table. We don't allow the ownership of those things to - * be changed separately from the parent table. Also, we can skip permission - * checks (this is necessary not just an optimization, else we'd fail to - * handle toast tables properly). - * - * recursing is also true if ALTER TYPE OWNER is calling us to fix up a - * free-standing composite type. ++ * Return the address of the new parent relation. */ -void -ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lockmode) +static ObjectAddress - ATExecSetCompression(Relation rel, - const char *column, - Node *newValue, - LOCKMODE lockmode) ++ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode) { - Relation attrel; - Relation target_rel; - Relation class_rel; -- HeapTuple tuple; - Form_pg_attribute atttableform; - AttrNumber attnum; - char *compression; - char cmethod; - Form_pg_class tuple_class; ++ Relation parent_rel; ++ List *children; + ObjectAddress address; ++ const char *trigger_name; - compression = strVal(newValue); + /* - * Get exclusive lock till end of transaction on the target table. Use - * relation_open so that we can work on indexes and sequences. ++ * A self-exclusive lock is needed here. See the similar case in ++ * MergeAttributes() for a full explanation. + */ - target_rel = relation_open(relationOid, lockmode); - - /* Get its pg_class tuple, too */ - class_rel = table_open(RelationRelationId, RowExclusiveLock); ++ parent_rel = table_openrv(parent, ShareUpdateExclusiveLock); - attrel = table_open(AttributeRelationId, RowExclusiveLock); - tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relationOid)); - if (!HeapTupleIsValid(tuple)) - elog(ERROR, "cache lookup failed for relation %u", relationOid); - tuple_class = (Form_pg_class) GETSTRUCT(tuple); ++ /* ++ * 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); - /* copy the cache entry so we can scribble on it below */ - tuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), column); - if (!HeapTupleIsValid(tuple)) - /* Can we change the ownership of this tuple? */ - switch (tuple_class->relkind) - { - case RELKIND_RELATION: - case RELKIND_VIEW: - case RELKIND_MATVIEW: - case RELKIND_FOREIGN_TABLE: - case RELKIND_PARTITIONED_TABLE: - /* ok to change owner */ - break; - case RELKIND_INDEX: - if (!recursing) - { - /* - * Because ALTER INDEX OWNER used to be allowed, and in fact - * is generated by old versions of pg_dump, we give a warning - * and do nothing rather than erroring out. Also, to avoid - * unnecessary chatter while restoring those old dumps, say - * nothing at all if the command would be a no-op anyway. - */ - if (tuple_class->relowner != newOwnerId) - ereport(WARNING, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot change owner of index \"%s\"", - NameStr(tuple_class->relname)), - errhint("Change the ownership of the index's table instead."))); - /* quick hack to exit via the no-op path */ - newOwnerId = tuple_class->relowner; - } - break; - case RELKIND_PARTITIONED_INDEX: - if (recursing) - break; - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot change owner of index \"%s\"", - NameStr(tuple_class->relname)), - errhint("Change the ownership of the index's table instead."))); - break; - case RELKIND_SEQUENCE: - if (!recursing && - tuple_class->relowner != newOwnerId) - { - /* if it's an owned sequence, disallow changing it by itself */ - Oid tableId; - int32 colId; ++ /* 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_UNDEFINED_COLUMN), - errmsg("column \"%s\" of relation \"%s\" does not exist", - column, RelationGetRelationName(rel)))); ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("cannot inherit from temporary relation \"%s\"", ++ RelationGetRelationName(parent_rel)))); - /* prevent them from altering a system attribute */ - atttableform = (Form_pg_attribute) GETSTRUCT(tuple); - attnum = atttableform->attnum; - if (attnum <= 0) - if (sequenceIsOwned(relationOid, DEPENDENCY_AUTO, &tableId, &colId) || - sequenceIsOwned(relationOid, DEPENDENCY_INTERNAL, &tableId, &colId)) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot change owner of sequence \"%s\"", - NameStr(tuple_class->relname)), - errdetail("Sequence \"%s\" is linked to table \"%s\".", - NameStr(tuple_class->relname), - get_rel_name(tableId)))); - } - break; - case RELKIND_COMPOSITE_TYPE: - if (recursing) - break; - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is a composite type", - NameStr(tuple_class->relname)), - /* translator: %s is an SQL ALTER command */ - errhint("Use %s instead.", - "ALTER TYPE"))); - break; - case RELKIND_TOASTVALUE: - if (recursing) - break; - /* FALL THRU */ - default: - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot change owner of relation \"%s\"", - NameStr(tuple_class->relname)), - errdetail_relkind_not_supported(tuple_class->relkind))); - } ++ /* If parent rel is temp, it must belong to this session */ ++ if (RELATION_IS_OTHER_TEMP(parent_rel)) + ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot alter system column \"%s\"", column))); ++ (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"))); /* - * Check that column type is compressible, then get the attribute - * compression method code - * If the new owner is the same as the existing owner, consider the - * command to have succeeded. This is for dump restoration purposes. ++ * 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. */ - cmethod = GetAttributeCompression(atttableform->atttypid, compression); - - /* update pg_attribute entry */ - atttableform->attcompression = cmethod; - CatalogTupleUpdate(attrel, &tuple->t_self, tuple); - if (tuple_class->relowner != newOwnerId) - { - Datum repl_val[Natts_pg_class]; - bool repl_null[Natts_pg_class]; - bool repl_repl[Natts_pg_class]; - Acl *newAcl; - Datum aclDatum; - bool isNull; - HeapTuple newtuple; ++ children = find_all_inheritors(RelationGetRelid(child_rel), ++ AccessShareLock, NULL); - InvokeObjectPostAlterHook(RelationRelationId, - RelationGetRelid(rel), - attnum); - /* skip permission checks when recursing to index or toast table */ - if (!recursing) - { - /* Superusers can always do it */ - if (!superuser()) - { - Oid namespaceOid = tuple_class->relnamespace; - AclResult aclresult; ++ 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)))); - /* Otherwise, must be owner of the existing object */ - if (!object_ownercheck(RelationRelationId, relationOid, GetUserId())) - aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relationOid)), - RelationGetRelationName(target_rel)); + /* - * Apply the change to indexes as well (only for simple index columns, - * matching behavior of index.c ConstructTupleDescriptor()). ++ * 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(). + */ - SetIndexStorageProperties(rel, attrel, attnum, - false, 0, - true, cmethod, - lockmode); ++ 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."))); - heap_freetuple(tuple); - /* Must be able to become new owner */ - check_can_set_role(GetUserId(), newOwnerId); ++ /* OK to create inheritance */ ++ CreateInheritance(child_rel, parent_rel, false); - table_close(attrel, RowExclusiveLock); - /* New owner must have CREATE privilege on namespace */ - aclresult = object_aclcheck(NamespaceRelationId, namespaceOid, newOwnerId, - ACL_CREATE); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_SCHEMA, - get_namespace_name(namespaceOid)); - } - } ++ ObjectAddressSet(address, RelationRelationId, ++ RelationGetRelid(parent_rel)); - /* make changes visible */ - CommandCounterIncrement(); - memset(repl_null, false, sizeof(repl_null)); - memset(repl_repl, false, sizeof(repl_repl)); ++ /* keep our lock on the parent relation until commit */ ++ table_close(parent_rel, NoLock); - ObjectAddressSubSet(address, RelationRelationId, - RelationGetRelid(rel), attnum); - repl_repl[Anum_pg_class_relowner - 1] = true; - repl_val[Anum_pg_class_relowner - 1] = ObjectIdGetDatum(newOwnerId); + return address; +} - - /* - * Determine the modified ACL for the new owner. This is only - * necessary when the ACL is non-null. - */ - aclDatum = SysCacheGetAttr(RELOID, tuple, - Anum_pg_class_relacl, - &isNull); - if (!isNull) - { - newAcl = aclnewowner(DatumGetAclP(aclDatum), - tuple_class->relowner, newOwnerId); - repl_repl[Anum_pg_class_relacl - 1] = true; - repl_val[Anum_pg_class_relacl - 1] = PointerGetDatum(newAcl); - } +/* - * Preparation phase for SET LOGGED/UNLOGGED ++ * CreateInheritance ++ * Catalog manipulation portion of creating inheritance between a child ++ * table and a parent table. + * - * 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. ++ * Common to ATExecAddInherit() and ATExecAttachPartition(). + */ - static void - ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged) ++void ++CreateInheritance(Relation child_rel, Relation parent_rel, bool ispartition) +{ - Relation pg_constraint; - HeapTuple tuple; ++ Relation catalogRelation; + 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 && - 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."))); ++ ScanKeyData key; ++ HeapTuple inheritsTuple; ++ int32 inhseqno; - /* - * 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); - newtuple = heap_modify_tuple(tuple, RelationGetDescr(class_rel), repl_val, repl_null, repl_repl); ++ /* Note: get RowExclusiveLock because we will write pg_inherits below. */ ++ catalogRelation = table_open(InheritsRelationId, RowExclusiveLock); - CatalogTupleUpdate(class_rel, &newtuple->t_self, newtuple); + /* - * Scan conrelid if changing to permanent, else confrelid. This also - * determines whether a useful index exists. ++ * 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(&skey[0], - toLogged ? Anum_pg_constraint_conrelid : - Anum_pg_constraint_confrelid, ++ ScanKeyInit(&key, ++ Anum_pg_inherits_inhrelid, + BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(RelationGetRelid(rel))); - scan = systable_beginscan(pg_constraint, - toLogged ? ConstraintRelidTypidNameIndexId : InvalidOid, - true, NULL, 1, skey); ++ ObjectIdGetDatum(RelationGetRelid(child_rel))); ++ scan = systable_beginscan(catalogRelation, InheritsRelidSeqnoIndexId, ++ true, NULL, 1, &key); - while (HeapTupleIsValid(tuple = systable_getnext(scan))) - heap_freetuple(newtuple); ++ /* inhseqno sequences start at 1 */ ++ inhseqno = 0; ++ while (HeapTupleIsValid(inheritsTuple = 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); ++ Form_pg_inherits inh = (Form_pg_inherits) GETSTRUCT(inheritsTuple); - 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)))); - } - /* - * We must similarly update any per-column ACLs to reflect the new - * owner; for neatness reasons that's split out as a subroutine. - */ - change_owner_fix_column_acls(relationOid, - tuple_class->relowner, - newOwnerId); ++ 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)))); - relation_close(foreignrel, AccessShareLock); - } - /* - * Update owner dependency reference, if any. A composite type has - * none, because it's tracked for the pg_type entry instead of here; - * indexes and TOAST tables don't have their own entries either. - */ - if (tuple_class->relkind != RELKIND_COMPOSITE_TYPE && - tuple_class->relkind != RELKIND_INDEX && - tuple_class->relkind != RELKIND_PARTITIONED_INDEX && - tuple_class->relkind != RELKIND_TOASTVALUE) - changeDependencyOnOwner(RelationRelationId, relationOid, - newOwnerId); ++ if (inh->inhseqno > inhseqno) ++ inhseqno = inh->inhseqno; + } - + systable_endscan(scan); - table_close(pg_constraint, AccessShareLock); - /* - * Also change the ownership of the table's row type, if it has one - */ - if (OidIsValid(tuple_class->reltype)) - AlterTypeOwnerInternal(tuple_class->reltype, newOwnerId); ++ /* Match up the columns and bump attinhcount as needed */ ++ MergeAttributesIntoExisting(child_rel, parent_rel, ispartition); - /* 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; - /* - * If we are operating on a table or materialized view, also change - * the ownership of any indexes and sequences that belong to the - * relation, as well as its toast table (if it has one). - */ - if (tuple_class->relkind == RELKIND_RELATION || - tuple_class->relkind == RELKIND_PARTITIONED_TABLE || - tuple_class->relkind == RELKIND_MATVIEW || - tuple_class->relkind == RELKIND_TOASTVALUE) - { - List *index_oid_list; - ListCell *i; ++ /* Match up the constraints and bump coninhcount as needed */ ++ MergeConstraintsIntoExisting(child_rel, parent_rel); + - /* Find all the indexes belonging to this relation */ - index_oid_list = RelationGetIndexList(target_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); + - /* For each index, recursively change its ownership */ - foreach(i, index_oid_list) - ATExecChangeOwner(lfirst_oid(i), newOwnerId, true, lockmode); ++ /* Now we're done with pg_inherits */ ++ table_close(catalogRelation, RowExclusiveLock); +} - list_free(index_oid_list); - } +/* - * Execute ALTER TABLE SET SCHEMA ++ * Obtain the source-text form of the constraint expression for a check ++ * constraint, given its pg_constraint tuple + */ - ObjectAddress - AlterTableNamespace(AlterObjectSchemaStmt *stmt, Oid *oldschema) ++static char * ++decompile_conbin(HeapTuple contup, TupleDesc tupdesc) +{ - Relation rel; - Oid relid; - Oid oldNspOid; - Oid nspOid; - RangeVar *newrv; - ObjectAddresses *objsMoved; - ObjectAddress myself; ++ Form_pg_constraint con; ++ bool isnull; ++ Datum attr; ++ Datum expr; - relid = RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock, - stmt->missing_ok ? RVR_MISSING_OK : 0, - RangeVarCallbackForAlterRelation, - stmt); - /* If it has a toast table, recurse to change its ownership */ - if (tuple_class->reltoastrelid != InvalidOid) - ATExecChangeOwner(tuple_class->reltoastrelid, newOwnerId, - true, lockmode); ++ con = (Form_pg_constraint) GETSTRUCT(contup); ++ attr = heap_getattr(contup, Anum_pg_constraint_conbin, tupdesc, &isnull); ++ if (isnull) ++ elog(ERROR, "null conbin for constraint %u", con->oid); - if (!OidIsValid(relid)) - { - ereport(NOTICE, - (errmsg("relation \"%s\" does not exist, skipping", - stmt->relation->relname))); - return InvalidObjectAddress; - /* If it has dependent sequences, recurse to change them too */ - change_owner_recurse_to_sequences(relationOid, newOwnerId, lockmode); -- } ++ expr = DirectFunctionCall2(pg_get_expr, attr, ++ ObjectIdGetDatum(con->conrelid)); ++ return TextDatumGetCString(expr); ++} - rel = relation_open(relid, NoLock); - InvokeObjectPostAlterHook(RelationRelationId, relationOid, 0); ++/* ++ * 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); - oldNspOid = RelationGetNamespace(rel); - ReleaseSysCache(tuple); - table_close(class_rel, RowExclusiveLock); - relation_close(target_rel, NoLock); ++ if (acon->condeferrable != bcon->condeferrable || ++ acon->condeferred != bcon->condeferred || ++ strcmp(decompile_conbin(a, tupleDesc), ++ decompile_conbin(b, tupleDesc)) != 0) ++ return false; ++ else ++ return true; + } - /* If it's an owned sequence, disallow moving it by itself. */ - if (rel->rd_rel->relkind == RELKIND_SEQUENCE) - { - Oid tableId; - int32 colId; + /* - * change_owner_fix_column_acls ++ * Check columns in child table match up with columns in parent, and increment ++ * their attinhcount. + * - * Helper function for ATExecChangeOwner. Scan the columns of the table - * and fix any non-null column ACLs to reflect the new owner. ++ * 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 -change_owner_fix_column_acls(Oid relationOid, Oid oldOwnerId, Oid newOwnerId) ++MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispartition) + { - Relation attRelation; - SysScanDesc scan; - ScanKeyData key[1]; - HeapTuple attributeTuple; ++ Relation attrrel; ++ TupleDesc parent_desc; - 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)))); - } - attRelation = table_open(AttributeRelationId, RowExclusiveLock); - ScanKeyInit(&key[0], - Anum_pg_attribute_attrelid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(relationOid)); - scan = systable_beginscan(attRelation, AttributeRelidNumIndexId, - true, NULL, 1, key); - while (HeapTupleIsValid(attributeTuple = systable_getnext(scan))) - { - Form_pg_attribute att = (Form_pg_attribute) GETSTRUCT(attributeTuple); - Datum repl_val[Natts_pg_attribute]; - bool repl_null[Natts_pg_attribute]; - bool repl_repl[Natts_pg_attribute]; - Acl *newAcl; - Datum aclDatum; - bool isNull; - HeapTuple newtuple; ++ attrrel = table_open(AttributeRelationId, RowExclusiveLock); ++ parent_desc = RelationGetDescr(parent_rel); - /* Get and lock schema OID and check its permissions. */ - newrv = makeRangeVar(stmt->newschema, RelationGetRelationName(rel), -1); - nspOid = RangeVarGetAndCheckCreationNamespace(newrv, NoLock, NULL); - /* Ignore dropped columns */ - if (att->attisdropped) - continue; ++ 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; - /* common checks on switching namespaces */ - CheckSetNamespace(oldNspOid, nspOid); - aclDatum = heap_getattr(attributeTuple, - Anum_pg_attribute_attacl, - RelationGetDescr(attRelation), - &isNull); - /* Null ACLs do not require changes */ - if (isNull) ++ /* Ignore dropped columns in the parent. */ ++ if (parent_att->attisdropped) + continue; - objsMoved = new_object_addresses(); - AlterTableNamespaceInternal(rel, oldNspOid, nspOid, objsMoved); - free_object_addresses(objsMoved); - memset(repl_null, false, sizeof(repl_null)); - memset(repl_repl, false, sizeof(repl_repl)); - - newAcl = aclnewowner(DatumGetAclP(aclDatum), - oldOwnerId, newOwnerId); - repl_repl[Anum_pg_attribute_attacl - 1] = true; - repl_val[Anum_pg_attribute_attacl - 1] = PointerGetDatum(newAcl); - - newtuple = heap_modify_tuple(attributeTuple, - RelationGetDescr(attRelation), - repl_val, repl_null, repl_repl); ++ /* 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); - ObjectAddressSet(myself, RelationRelationId, relid); - CatalogTupleUpdate(attRelation, &newtuple->t_self, newtuple); ++ 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 (oldschema) - *oldschema = oldNspOid; - heap_freetuple(newtuple); - } - systable_endscan(scan); - table_close(attRelation, RowExclusiveLock); -} ++ 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))); - /* close rel, but keep lock until commit */ - relation_close(rel, NoLock); -/* - * change_owner_recurse_to_sequences - * - * Helper function for ATExecChangeOwner. Examines pg_depend searching - * for sequences that are dependent on serial columns, and changes their - * ownership. - */ -static void -change_owner_recurse_to_sequences(Oid relationOid, Oid newOwnerId, LOCKMODE lockmode) -{ - Relation depRel; - SysScanDesc scan; - ScanKeyData key[2]; - HeapTuple tup; ++ /* ++ * 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; - return myself; - } - /* - * 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); ++ 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))); ++ } - /* - * 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; - ScanKeyInit(&key[0], - Anum_pg_depend_refclassid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(RelationRelationId)); - ScanKeyInit(&key[1], - Anum_pg_depend_refobjid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(relationOid)); - /* we leave refobjsubid unspecified */ ++ /* ++ * 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))); - Assert(objsMoved != NULL); - scan = systable_beginscan(depRel, DependReferenceIndexId, true, - NULL, 2, key); ++ 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"))); - /* OK, modify the pg_class row and pg_depend entry */ - classRel = table_open(RelationRelationId, RowExclusiveLock); - while (HeapTupleIsValid(tup = systable_getnext(scan))) - { - Form_pg_depend depForm = (Form_pg_depend) GETSTRUCT(tup); - Relation seqRel; ++ /* ++ * 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; - AlterRelationNamespaceInternal(classRel, RelationGetRelid(rel), oldNspOid, - nspOid, true, objsMoved); - /* 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; ++ /* ++ * 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")); - /* 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); - /* Use relation_open just in case it's an index */ - seqRel = relation_open(depForm->objid, lockmode); ++ /* ++ * 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; ++ } - /* Fix other dependent stuff */ - AlterIndexNamespaces(classRel, rel, oldNspOid, nspOid, objsMoved); - AlterSeqNamespaces(classRel, rel, oldNspOid, nspOid, - objsMoved, AccessExclusiveLock); - AlterConstraintNamespaces(RelationGetRelid(rel), oldNspOid, nspOid, - false, objsMoved); - /* skip non-sequence relations */ - if (RelationGetForm(seqRel)->relkind != RELKIND_SEQUENCE) ++ CatalogTupleUpdate(attrrel, &tuple->t_self, tuple); ++ heap_freetuple(tuple); ++ } ++ else + { - /* No need to keep the lock */ - relation_close(seqRel, lockmode); - continue; ++ ereport(ERROR, ++ (errcode(ERRCODE_DATATYPE_MISMATCH), ++ errmsg("child table is missing column \"%s\"", parent_attname))); + } - - /* We don't need to close the sequence while we alter it. */ - ATExecChangeOwner(depForm->objid, newOwnerId, true, lockmode); - - /* Now we can close it. Keep the lock till end of transaction. */ - relation_close(seqRel, NoLock); + } - table_close(classRel, RowExclusiveLock); - systable_endscan(scan); - - relation_close(depRel, AccessShareLock); ++ table_close(attrrel, 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. - * ALTER TABLE CLUSTER ON ++ * Check constraints in child table match up with constraints in parent, ++ * and increment their coninhcount. + * - * The only thing we have to do is to change the indisclustered bits. ++ * Constraints that are marked ONLY in the parent are ignored. + * - * Return the address of the new clustering index. ++ * 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. */ - void - AlterRelationNamespaceInternal(Relation classRel, Oid relOid, - Oid oldNspOid, Oid newNspOid, - bool hasDependEntry, - ObjectAddresses *objsMoved) -static ObjectAddress -ATExecClusterOn(Relation rel, const char *indexName, LOCKMODE lockmode) ++static void ++MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel) { - HeapTuple classTup; - Form_pg_class classForm; - ObjectAddress thisobj; - bool already_done = false; - Oid indexOid; - ObjectAddress address; ++ Relation constraintrel; ++ SysScanDesc parent_scan; ++ ScanKeyData parent_key; ++ HeapTuple parent_tuple; ++ Oid parent_relid = RelationGetRelid(parent_rel); ++ AttrMap *attmap; - /* 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); - indexOid = get_relname_relid(indexName, rel->rd_rel->relnamespace); ++ constraintrel = table_open(ConstraintRelationId, RowExclusiveLock); - Assert(classForm->relnamespace == oldNspOid); - if (!OidIsValid(indexOid)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("index \"%s\" for table \"%s\" does not exist", - indexName, RelationGetRelationName(rel)))); ++ /* 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); - thisobj.classId = RelationRelationId; - thisobj.objectId = relOid; - thisobj.objectSubId = 0; - /* Check index is valid to cluster on */ - check_index_is_clusterable(rel, indexOid, lockmode); ++ attmap = build_attrmap_by_name(RelationGetDescr(parent_rel), ++ RelationGetDescr(child_rel), ++ true); - /* - * 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) - /* And do the work */ - mark_index_clustered(rel, indexOid, false); ++ while (HeapTupleIsValid(parent_tuple = systable_getnext(parent_scan))) + { - 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); ++ 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; - ObjectAddressSet(address, - RelationRelationId, indexOid); ++ if (parent_con->contype != CONSTRAINT_CHECK && ++ parent_con->contype != CONSTRAINT_NOTNULL) ++ continue; - /* 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); - return address; -} ++ /* if the parent's constraint is marked NO INHERIT, it's not inherited */ ++ if (parent_con->connoinherit) ++ continue; - InvokeObjectPostAlterHook(RelationRelationId, relOid, 0); - } -/* - * ALTER TABLE SET WITHOUT CLUSTER - * - * We have to find any indexes on the table that have indisclustered bit - * set and turn it off. - */ -static void -ATExecDropCluster(Relation rel, LOCKMODE lockmode) -{ - mark_index_clustered(rel, InvalidOid, false); -} ++ if (parent_con->contype == CONSTRAINT_NOTNULL) ++ parent_attno = extractNotNullColumn(parent_tuple); ++ else ++ parent_attno = InvalidAttrNumber; - heap_freetuple(classTup); - } -/* - * Preparation phase for SET ACCESS METHOD - * - * Check that the access method exists and determine whether a change is - * actually needed. - */ -static void -ATPrepSetAccessMethod(AlteredTableInfo *tab, Relation rel, const char *amname) -{ - Oid amoid; ++ /* 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); - /* - * 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; - /* - * Look up the access method name and check that it differs from the - * table's current AM. If DEFAULT was specified for a partitioned table - * (amname is NULL), set it to InvalidOid to reset the catalogued AM. - */ - if (amname != NULL) - amoid = get_table_am_oid(amname, false); - else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) - amoid = InvalidOid; - else - amoid = get_table_am_oid(default_table_access_method, false); ++ while (HeapTupleIsValid(child_tuple = systable_getnext(child_scan))) ++ { ++ Form_pg_constraint child_con = (Form_pg_constraint) GETSTRUCT(child_tuple); ++ HeapTuple child_copy; - indexList = RelationGetIndexList(rel); - /* if it's a match, phase 3 doesn't need to do anything */ - if (rel->rd_rel->relam == amoid) - return; ++ if (child_con->contype != parent_con->contype) ++ continue; - foreach(l, indexList) - { - Oid indexOid = lfirst_oid(l); - ObjectAddress thisobj; - /* Save info for Phase 3 to do the real work */ - tab->rewrite |= AT_REWRITE_ACCESS_METHOD; - tab->newAccessMethod = amoid; - tab->chgAccessMethod = true; -} ++ /* ++ * 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; - thisobj.classId = RelationRelationId; - thisobj.objectId = indexOid; - thisobj.objectSubId = 0; -/* - * Special handling of ALTER TABLE SET ACCESS METHOD for relations with no - * storage that have an interest in preserving AM. - * - * Since these have no storage, setting the access method is a catalog only - * operation. - */ -static void -ATExecSetAccessMethodNoStorage(Relation rel, Oid newAccessMethodId) -{ - Relation pg_class; - Oid oldAccessMethodId; - HeapTuple tuple; - Form_pg_class rd_rel; - Oid reloid = RelationGetRelid(rel); ++ parent_attr = TupleDescAttr(parent_rel->rd_att, parent_attno - 1); ++ child_attno = extractNotNullColumn(child_tuple); ++ if (parent_attno != attmap->attnums[child_attno - 1]) ++ continue; - /* - * 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); - } - } - /* - * Shouldn't be called on relations having storage; these are processed in - * phase 3. - */ - Assert(!RELKIND_HAS_STORAGE(rel->rd_rel->relkind)); ++ 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"); ++ } - list_free(indexList); - } - /* Get a modifiable copy of the relation's pg_class row. */ - pg_class = table_open(RelationRelationId, RowExclusiveLock); ++ 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)))); - /* - * 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; - tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid)); - if (!HeapTupleIsValid(tuple)) - elog(ERROR, "cache lookup failed for relation %u", reloid); - rd_rel = (Form_pg_class) GETSTRUCT(tuple); ++ /* ++ * 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)))); - /* - * 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); - /* Update the pg_class row. */ - oldAccessMethodId = rd_rel->relam; - rd_rel->relam = newAccessMethodId; ++ /* ++ * 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)))); - 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 */ - /* Leave if no update required */ - if (rd_rel->relam == oldAccessMethodId) - { - heap_freetuple(tuple); - table_close(pg_class, RowExclusiveLock); - return; - } ++ /* ++ * 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)))); - scan = systable_beginscan(depRel, DependReferenceIndexId, true, - NULL, 2, key); - CatalogTupleUpdate(pg_class, &tuple->t_self, tuple); ++ /* ++ * 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); - while (HeapTupleIsValid(tup = systable_getnext(scan))) - /* - * Update the dependency on the new access method. No dependency is added - * if the new access method is InvalidOid (default case). Be very careful - * that this has to compare the previous value stored in pg_class with the - * new one. - */ - if (!OidIsValid(oldAccessMethodId) && OidIsValid(rd_rel->relam)) -- { - Form_pg_depend depForm = (Form_pg_depend) GETSTRUCT(tup); - Relation seqRel; - ObjectAddress relobj, - referenced; ++ if (pg_add_s16_overflow(child_con->coninhcount, 1, ++ &child_con->coninhcount)) ++ ereport(ERROR, ++ errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), ++ errmsg("too many inheritance parents")); - /* 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; - /* - * New access method is defined and there was no dependency - * previously, so record a new one. - */ - ObjectAddressSet(relobj, RelationRelationId, reloid); - ObjectAddressSet(referenced, AccessMethodRelationId, rd_rel->relam); - recordDependencyOn(&relobj, &referenced, DEPENDENCY_NORMAL); - } - else if (OidIsValid(oldAccessMethodId) && - !OidIsValid(rd_rel->relam)) - { - /* - * There was an access method defined, and no new one, so just remove - * the existing dependency. - */ - deleteDependencyRecordsForClass(RelationRelationId, reloid, - AccessMethodRelationId, - DEPENDENCY_NORMAL); - } - else - { - Assert(OidIsValid(oldAccessMethodId) && - OidIsValid(rd_rel->relam)); ++ /* ++ * 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; ++ } + - /* Use relation_open just in case it's an index */ - seqRel = relation_open(depForm->objid, lockmode); ++ CatalogTupleUpdate(constraintrel, &child_copy->t_self, child_copy); ++ heap_freetuple(child_copy); + - /* skip non-sequence relations */ - if (RelationGetForm(seqRel)->relkind != RELKIND_SEQUENCE) - { - /* No need to keep the lock */ - relation_close(seqRel, lockmode); - continue; ++ found = true; ++ break; + } - /* Fix the pg_class and pg_depend entries */ - AlterRelationNamespaceInternal(classRel, depForm->objid, - oldNspOid, newNspOid, - true, objsMoved); - /* Both are valid, so update the dependency */ - changeDependencyFor(RelationRelationId, reloid, - AccessMethodRelationId, - oldAccessMethodId, rd_rel->relam); - } ++ systable_endscan(child_scan); - /* - * 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); - /* make the relam and dependency changes visible */ - CommandCounterIncrement(); ++ 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))); - /* Now we can close it. Keep the lock till end of transaction. */ - relation_close(seqRel, NoLock); - InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0); ++ ereport(ERROR, ++ (errcode(ERRCODE_DATATYPE_MISMATCH), ++ errmsg("child table is missing constraint \"%s\"", ++ NameStr(parent_con->conname)))); ++ } + } - systable_endscan(scan); - - relation_close(depRel, AccessShareLock); - heap_freetuple(tuple); - table_close(pg_class, RowExclusiveLock); ++ systable_endscan(parent_scan); ++ table_close(constraintrel, RowExclusiveLock); } - /* - * This code supports - * CREATE TEMP TABLE ... ON COMMIT { DROP | PRESERVE ROWS | DELETE ROWS } - * ALTER TABLE SET TABLESPACE ++ * ALTER TABLE NO INHERIT + * - * 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. ++ * Return value is the address of the relation that is no longer parent. */ - void - register_on_commit_action(Oid relid, OnCommitAction action) -static void -ATPrepSetTableSpace(AlteredTableInfo *tab, Relation rel, const char *tablespacename, LOCKMODE lockmode) ++static ObjectAddress ++ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode) { - OnCommitItem *oc; - MemoryContext oldcxt; - Oid tablespaceId; - - /* Check that the tablespace exists */ - tablespaceId = get_tablespace_oid(tablespacename, false); ++ ObjectAddress address; ++ Relation parent_rel; + - /* Check permissions except when moving to database's default */ - if (OidIsValid(tablespaceId) && tablespaceId != MyDatabaseTableSpace) - { - AclResult aclresult; ++ if (rel->rd_rel->relispartition) ++ ereport(ERROR, ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("cannot change inheritance of a partition"))); - aclresult = object_aclcheck(TableSpaceRelationId, tablespaceId, GetUserId(), ACL_CREATE); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_TABLESPACE, tablespacename); - } + /* - * We needn't bother registering the relation unless there is an ON COMMIT - * action we need to take. ++ * 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. + */ ++<<<<<<< ours + if (action == ONCOMMIT_NOOP || action == ONCOMMIT_PRESERVE_ROWS) + return; - /* Save info for Phase 3 to do the real work */ - if (OidIsValid(tab->newTableSpace)) - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("cannot have multiple SET TABLESPACE subcommands"))); + oldcxt = MemoryContextSwitchTo(CacheMemoryContext); - tab->newTableSpace = tablespaceId; -} + oc = palloc_object(OnCommitItem); + oc->relid = relid; + oc->oncommit = action; + oc->creating_subid = GetCurrentSubTransactionId(); + oc->deleting_subid = InvalidSubTransactionId; ++======= ++ parent_rel = table_openrv(parent, AccessShareLock); ++>>>>>>> theirs -/* - * Set, reset, or replace reloptions. - */ -static void -ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation, - LOCKMODE lockmode) -{ - Oid relid; - Relation pgclass; - HeapTuple tuple; - HeapTuple newtuple; - Datum datum; - Datum newOptions; - Datum repl_val[Natts_pg_class]; - bool repl_null[Natts_pg_class]; - bool repl_repl[Natts_pg_class]; - const char *const validnsps[] = HEAP_RELOPT_NAMESPACES; + /* - * 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. ++ * We don't bother to check ownership of the parent table --- ownership of ++ * the child is presumed enough rights. + */ - on_commits = lcons(oc, on_commits); - MemoryContextSwitchTo(oldcxt); - } - if (defList == NIL && operation != AT_ReplaceRelOptions) - return; /* nothing to do */ ++ /* Off to RemoveInheritance() where most of the work happens */ ++ RemoveInheritance(rel, parent_rel, false); - /* - * 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; - pgclass = table_open(RelationRelationId, RowExclusiveLock); ++ ObjectAddressSet(address, RelationRelationId, ++ RelationGetRelid(parent_rel)); - foreach(l, on_commits) - { - OnCommitItem *oc = (OnCommitItem *) lfirst(l); - /* Fetch heap tuple */ - relid = RelationGetRelid(rel); - tuple = SearchSysCacheLocked1(RELOID, ObjectIdGetDatum(relid)); - if (!HeapTupleIsValid(tuple)) - elog(ERROR, "cache lookup failed for relation %u", relid); ++ /* keep our lock on the parent relation until commit */ ++ table_close(parent_rel, NoLock); - if (oc->relid == relid) - { - oc->deleting_subid = GetCurrentSubTransactionId(); - break; - } - if (operation == AT_ReplaceRelOptions) - { - /* - * If we're supposed to replace the reloptions list, we just pretend - * there were none before. - */ - datum = (Datum) 0; -- } - else - { - bool isnull; ++ return address; +} - /* Get the old reloptions */ - datum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, - &isnull); - if (isnull) - datum = (Datum) 0; - } +/* - * Perform ON COMMIT actions. ++ * RemoveInheritance + * - * This is invoked just before actually committing, since it's possible - * to encounter errors. ++ * 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(). + */ +void - PreCommit_on_commit_actions(void) ++RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached) +{ - ListCell *l; - List *oids_to_truncate = NIL; - List *oids_to_drop = NIL; ++ Relation catalogRelation; ++ SysScanDesc scan; ++ ScanKeyData key[3]; ++ HeapTuple attributeTuple, ++ constraintTuple; ++ AttrMap *attmap; ++ List *connames; ++ List *nncolumns; ++ bool found; ++ bool is_partitioning; - foreach(l, on_commits) - /* Generate new proposed reloptions (text array) */ - newOptions = transformRelOptions(datum, defList, NULL, validnsps, false, - operation == AT_ResetRelOptions); ++ is_partitioning = (parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); + - /* Validate */ - switch (rel->rd_rel->relkind) ++ found = DeleteInheritsTuple(RelationGetRelid(child_rel), ++ RelationGetRelid(parent_rel), ++ expect_detached, ++ RelationGetRelationName(child_rel)); ++ if (!found) { - OnCommitItem *oc = (OnCommitItem *) lfirst(l); - case RELKIND_RELATION: - case RELKIND_MATVIEW: - (void) heap_reloptions(rel->rd_rel->relkind, newOptions, true); - break; - case RELKIND_PARTITIONED_TABLE: - (void) partitioned_table_reloptions(newOptions, true); - break; - case RELKIND_VIEW: - (void) view_reloptions(newOptions, true); - break; - case RELKIND_INDEX: - case RELKIND_PARTITIONED_INDEX: - (void) index_reloptions(rel->rd_indam->amoptions, newOptions, true); - break; - case RELKIND_TOASTVALUE: - /* fall through to error -- shouldn't ever get here */ - default: ++ if (is_partitioning) + ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot set options for relation \"%s\"", - RelationGetRelationName(rel)), - errdetail_relkind_not_supported(rel->rd_rel->relkind))); - break; ++ (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)))); + } - /* Ignore entry if already dropped in this xact */ - if (oc->deleting_subid != InvalidSubTransactionId) - /* Special-case validation of view options */ - if (rel->rd_rel->relkind == RELKIND_VIEW) ++ /* ++ * 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))) + { - Query *view_query = get_view_query(rel); - List *view_options = untransformRelOptions(newOptions); - ListCell *cell; - bool check_option = false; - - foreach(cell, view_options) - { - DefElem *defel = (DefElem *) lfirst(cell); ++ Form_pg_attribute att = (Form_pg_attribute) GETSTRUCT(attributeTuple); + - if (strcmp(defel->defname, "check_option") == 0) - check_option = true; - } ++ /* Ignore if dropped or not inherited */ ++ if (att->attisdropped) ++ continue; ++ if (att->attinhcount <= 0) + continue; - switch (oc->oncommit) - /* - * If the check option is specified, look to see if the view is - * actually auto-updatable or not. - */ - if (check_option) ++ if (SearchSysCacheExistsAttName(RelationGetRelid(parent_rel), ++ NameStr(att->attname))) { - case ONCOMMIT_NOOP: - case ONCOMMIT_PRESERVE_ROWS: - /* Do nothing (there shouldn't be such entries, actually) */ - break; - case ONCOMMIT_DELETE_ROWS: - const char *view_updatable_error = - view_query_is_auto_updatable(view_query, true); ++ /* Decrement inhcount and possibly set islocal to true */ ++ HeapTuple copyTuple = heap_copytuple(attributeTuple); ++ Form_pg_attribute copy_att = (Form_pg_attribute) GETSTRUCT(copyTuple); - /* - * 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; - if (view_updatable_error) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("WITH CHECK OPTION is supported only on automatically updatable views"), - errhint("%s", _(view_updatable_error)))); ++ 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); /* - * 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. - * All we need do here is update the pg_class row; the new options will be - * propagated into relcaches during post-commit cache inval. ++ * 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. */ - if (oids_to_truncate != NIL) - heap_truncate(oids_to_truncate); - memset(repl_val, 0, sizeof(repl_val)); - memset(repl_null, false, sizeof(repl_null)); - memset(repl_repl, false, sizeof(repl_repl)); - - if (newOptions != (Datum) 0) - repl_val[Anum_pg_class_reloptions - 1] = newOptions; - else - repl_null[Anum_pg_class_reloptions - 1] = true; ++ attmap = build_attrmap_by_name(RelationGetDescr(child_rel), ++ RelationGetDescr(parent_rel), ++ false); - if (oids_to_drop != NIL) - repl_repl[Anum_pg_class_reloptions - 1] = true; ++ 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); + - newtuple = heap_modify_tuple(tuple, RelationGetDescr(pgclass), - repl_val, repl_null, repl_repl); ++ connames = NIL; ++ nncolumns = NIL; + - CatalogTupleUpdate(pgclass, &newtuple->t_self, newtuple); - UnlockTuple(pgclass, &tuple->t_self, InplaceUpdateTupleLock); ++ while (HeapTupleIsValid(constraintTuple = systable_getnext(scan))) + { - ObjectAddresses *targetObjects = new_object_addresses(); ++ Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(constraintTuple); - foreach(l, oids_to_drop) - InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0); ++ if (con->connoinherit) ++ continue; + - heap_freetuple(newtuple); ++ if (con->contype == CONSTRAINT_CHECK) ++ connames = lappend(connames, pstrdup(NameStr(con->conname))); ++ if (con->contype == CONSTRAINT_NOTNULL) + { - ObjectAddress object; ++ AttrNumber parent_attno = extractNotNullColumn(constraintTuple); - object.classId = RelationRelationId; - object.objectId = lfirst_oid(l); - object.objectSubId = 0; - ReleaseSysCache(tuple); ++ nncolumns = lappend_int(nncolumns, attmap->attnums[parent_attno - 1]); ++ } ++ } - Assert(!object_address_present(&object, targetObjects)); - /* repeat the whole exercise for the toast table, if there's one */ - if (OidIsValid(rel->rd_rel->reltoastrelid)) - { - Relation toastrel; - Oid toastid = rel->rd_rel->reltoastrelid; ++ systable_endscan(scan); - add_exact_object_address(&object, targetObjects); - } - toastrel = table_open(toastid, lockmode); ++ /* 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); - /* - * Object deletion might involve toast table access (to clean up - * toasted catalog entries), so ensure we have a valid snapshot. - */ - PushActiveSnapshot(GetTransactionSnapshot()); - /* Fetch heap tuple */ - tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(toastid)); - if (!HeapTupleIsValid(tuple)) - elog(ERROR, "cache lookup failed for relation %u", toastid); ++ while (HeapTupleIsValid(constraintTuple = systable_getnext(scan))) ++ { ++ Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(constraintTuple); ++ bool match = false; - if (operation == AT_ReplaceRelOptions) + /* - * Since this is an automatic drop, rather than one directly initiated - * by the user, we pass the PERFORM_DELETION_INTERNAL flag. ++ * Match CHECK constraints by name, not-null constraints by column ++ * number, and ignore all others. + */ - performMultipleDeletions(targetObjects, DROP_CASCADE, - PERFORM_DELETION_INTERNAL | PERFORM_DELETION_QUIETLY); - - PopActiveSnapshot(); ++ if (con->contype == CONSTRAINT_CHECK) + { - /* - * If we're supposed to replace the reloptions list, we just - * pretend there were none before. - */ - datum = (Datum) 0; ++ 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 ++ else if (con->contype == CONSTRAINT_NOTNULL) + { - bool isnull; ++ AttrNumber child_attno = extractNotNullColumn(constraintTuple); - #ifdef USE_ASSERT_CHECKING - /* Get the old reloptions */ - datum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, - &isnull); - if (isnull) - datum = (Datum) 0; ++ foreach_int(prevattno, nncolumns) ++ { ++ if (prevattno == child_attno) ++ { ++ match = true; ++ nncolumns = foreach_delete_current(nncolumns, prevattno); ++ break; ++ } ++ } + } - - newOptions = transformRelOptions(datum, defList, "toast", validnsps, - false, operation == AT_ResetRelOptions); - - (void) heap_reloptions(RELKIND_TOASTVALUE, newOptions, true); - - memset(repl_val, 0, sizeof(repl_val)); - memset(repl_null, false, sizeof(repl_null)); - memset(repl_repl, false, sizeof(repl_repl)); - - if (newOptions != (Datum) 0) - repl_val[Anum_pg_class_reloptions - 1] = newOptions; + else - repl_null[Anum_pg_class_reloptions - 1] = true; - - repl_repl[Anum_pg_class_reloptions - 1] = true; - - newtuple = heap_modify_tuple(tuple, RelationGetDescr(pgclass), - repl_val, repl_null, repl_repl); - - CatalogTupleUpdate(pgclass, &newtuple->t_self, newtuple); - - InvokeObjectPostAlterHookArg(RelationRelationId, - RelationGetRelid(toastrel), 0, - InvalidOid, true); - - heap_freetuple(newtuple); - - ReleaseSysCache(tuple); - - table_close(toastrel, NoLock); - } - - table_close(pgclass, RowExclusiveLock); -} - -/* - * Execute ALTER TABLE SET TABLESPACE for cases where there is no tuple - * rewriting to be done, so we just want to copy the data as fast as possible. - */ -static void -ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode) -{ - Relation rel; - Oid reltoastrelid; - RelFileNumber newrelfilenumber; - RelFileLocator newrlocator; - List *reltoastidxids = NIL; - ListCell *lc; ++ continue; - /* - * Note that table deletion will call remove_on_commit_action, so the - * entry should get marked as deleted. - */ - foreach(l, on_commits) - /* - * Need lock here in case we are recursing to toast table or index - */ - rel = relation_open(tableOid, lockmode); ++ if (match) + { - OnCommitItem *oc = (OnCommitItem *) lfirst(l); ++ /* Decrement inhcount and possibly set islocal to true */ ++ HeapTuple copyTuple = heap_copytuple(constraintTuple); ++ Form_pg_constraint copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); + - /* Check first if relation can be moved to new tablespace */ - if (!CheckRelationTableSpaceMove(rel, newTableSpace)) - { - InvokeObjectPostAlterHook(RelationRelationId, - RelationGetRelid(rel), 0); - relation_close(rel, NoLock); - return; - } ++ if (copy_con->coninhcount <= 0) /* shouldn't happen */ ++ elog(ERROR, "relation %u has non-inherited constraint \"%s\"", ++ RelationGetRelid(child_rel), NameStr(copy_con->conname)); - if (oc->oncommit != ONCOMMIT_DROP) - continue; - reltoastrelid = rel->rd_rel->reltoastrelid; - /* Fetch the list of indexes on toast relation if necessary */ - if (OidIsValid(reltoastrelid)) - { - Relation toastRel = relation_open(reltoastrelid, lockmode); ++ copy_con->coninhcount--; ++ if (copy_con->coninhcount == 0) ++ copy_con->conislocal = true; - Assert(oc->deleting_subid != InvalidSubTransactionId); - reltoastidxids = RelationGetIndexList(toastRel); - relation_close(toastRel, lockmode); ++ CatalogTupleUpdate(catalogRelation, ©Tuple->t_self, copyTuple); ++ heap_freetuple(copyTuple); + } - #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; - /* - * Relfilenumbers are not unique in databases across tablespaces, so we - * need to allocate a new one in the new tablespace. - */ - newrelfilenumber = GetNewRelFileNumber(newTableSpace, NULL, - rel->rd_rel->relpersistence); ++ /* 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)); - foreach(cur_item, on_commits) - { - OnCommitItem *oc = (OnCommitItem *) lfirst(cur_item); - /* Open old and new relation */ - newrlocator = rel->rd_locator; - newrlocator.relNumber = newrelfilenumber; - newrlocator.spcOid = newTableSpace; ++ systable_endscan(scan); ++ table_close(catalogRelation, RowExclusiveLock); - 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; - } - /* 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); -- } ++ drop_parent_dependency(RelationGetRelid(child_rel), ++ RelationRelationId, ++ RelationGetRelid(parent_rel), ++ child_dependency_type(is_partitioning)); + + /* - * 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(). ++ * 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. + */ - SetRelationTableSpace(rel, newTableSpace, newrelfilenumber); - - InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0); - - RelationAssumeNewRelfilelocator(rel); - - relation_close(rel, NoLock); - - /* Make sure the reltablespace change is visible */ - CommandCounterIncrement(); - - /* Move associated toast relation and/or indexes, too */ - if (OidIsValid(reltoastrelid)) - ATExecSetTableSpace(reltoastrelid, newTableSpace, lockmode); - foreach(lc, reltoastidxids) - ATExecSetTableSpace(lfirst_oid(lc), newTableSpace, lockmode); - - /* Clean up */ - list_free(reltoastidxids); ++ InvokeObjectPostAlterHookArg(InheritsRelationId, ++ RelationGetRelid(child_rel), 0, ++ RelationGetRelid(parent_rel), false); } /* - * Post-subcommit or post-subabort cleanup for ON COMMIT management. - * Special handling of ALTER TABLE SET TABLESPACE for relations with no - * storage that have an interest in preserving tablespace. -- * - * 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. - * Since these have no storage the tablespace can be updated with a simple - * metadata only operation to update the tablespace. ++ * 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. */ - void - AtEOSubXact_on_commit_actions(bool isCommit, SubTransactionId mySubid, - SubTransactionId parentSubid) + static void -ATExecSetTableSpaceNoStorage(Relation rel, Oid newTableSpace) ++drop_parent_dependency(Oid relid, Oid refclassid, Oid refobjid, ++ DependencyType deptype) { - ListCell *cur_item; - /* - * Shouldn't be called on relations having storage; these are processed in - * phase 3. - */ - Assert(!RELKIND_HAS_STORAGE(rel->rd_rel->relkind)); ++ Relation catalogRelation; ++ SysScanDesc scan; ++ ScanKeyData key[3]; ++ HeapTuple depTuple; - foreach(cur_item, on_commits) - /* check if relation can be moved to its new tablespace */ - if (!CheckRelationTableSpaceMove(rel, newTableSpace)) - { - InvokeObjectPostAlterHook(RelationRelationId, - RelationGetRelid(rel), - 0); - return; - } ++ catalogRelation = table_open(DependRelationId, RowExclusiveLock); + - /* Update can be done, so change reltablespace */ - SetRelationTableSpace(rel, newTableSpace, InvalidOid); ++ 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)); + - InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0); ++ scan = systable_beginscan(catalogRelation, DependDependerIndexId, true, ++ NULL, 3, key); + - /* Make sure the reltablespace change is visible */ - CommandCounterIncrement(); ++ while (HeapTupleIsValid(depTuple = systable_getnext(scan))) + { - OnCommitItem *oc = (OnCommitItem *) lfirst(cur_item); ++ Form_pg_depend dep = (Form_pg_depend) GETSTRUCT(depTuple); + - 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; - } ++ 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); } /* - * 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. - * Alter Table ALL ... SET TABLESPACE ++ * ALTER TABLE OF + * - * 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. ++ * 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. + * - * All to-be-moved objects are locked first. If NOWAIT is specified and the - * lock can't be acquired then we ereport(ERROR). ++ * The address of the type is returned. */ - void - RangeVarCallbackMaintainsTable(const RangeVar *relation, - Oid relId, Oid oldRelId, void *arg) -Oid -AlterTableMoveAll(AlterTableMoveAllStmt *stmt) ++static ObjectAddress ++ATExecAddOf(Relation rel, const TypeName *ofTypename, LOCKMODE lockmode) { - char relkind; - AclResult aclresult; - List *relations = NIL; - ListCell *l; - ScanKeyData key[1]; - Relation rel; - TableScanDesc scan; - HeapTuple tuple; - Oid orig_tablespaceoid; - Oid new_tablespaceoid; - List *role_oids = roleSpecsToIds(stmt->roles); - - /* Ensure we were not asked to move something we can't */ - if (stmt->objtype != OBJECT_TABLE && stmt->objtype != OBJECT_INDEX && - stmt->objtype != OBJECT_MATVIEW) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("only tables, indexes, and materialized views exist in tablespaces"))); ++ 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; - /* Nothing to do if the relation was not found. */ - if (!OidIsValid(relId)) - return; - /* 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); ++ /* Validate the type. */ ++ typetuple = typenameType(NULL, ofTypename, NULL); ++ check_of_type(typetuple); ++ typeform = (Form_pg_type) GETSTRUCT(typetuple); ++ typeid = typeform->oid; - /* - * 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) - /* 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) ++ /* 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_PARAMETER_VALUE), - errmsg("cannot move relations in to or out of pg_global tablespace"))); - - /* - * Must have CREATE rights on the new tablespace, unless it is the - * database default tablespace (which all users implicitly have CREATE - * rights on). - */ - if (OidIsValid(new_tablespaceoid) && new_tablespaceoid != MyDatabaseTableSpace) - { - AclResult aclresult; - - aclresult = object_aclcheck(TableSpaceRelationId, new_tablespaceoid, GetUserId(), - ACL_CREATE); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_TABLESPACE, - get_tablespace_name(new_tablespaceoid)); - } - - /* - * Now that the checks are done, check if we should set either to - * InvalidOid because it is our database's default tablespace. - */ - if (orig_tablespaceoid == MyDatabaseTableSpace) - orig_tablespaceoid = InvalidOid; - - if (new_tablespaceoid == MyDatabaseTableSpace) - new_tablespaceoid = InvalidOid; - - /* no-op */ - if (orig_tablespaceoid == new_tablespaceoid) - return new_tablespaceoid; + (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is not a table or materialized view", relation->relname))); ++ errmsg("typed tables cannot inherit"))); ++ systable_endscan(scan); ++ table_close(inheritsRelation, AccessShareLock); - /* 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); - } + /* - * Walk the list of objects in the tablespace and move them. This will - * only find objects in our database, of course. ++ * Check the tuple descriptors for compatibility. Unlike inheritance, we ++ * require that the order also match. However, attnotnull need not match. + */ - ScanKeyInit(&key[0], - Anum_pg_class_reltablespace, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(orig_tablespaceoid)); - - rel = table_open(RelationRelationId, AccessShareLock); - scan = table_beginscan_catalog(rel, 1, key); - while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) ++ typeTupleDesc = lookup_rowtype_tupdesc(typeid, -1); ++ tableTupleDesc = RelationGetDescr(rel); ++ table_attno = 1; ++ for (type_attno = 1; type_attno <= typeTupleDesc->natts; type_attno++) + { - Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple); - Oid relOid = relForm->oid; - - /* - * Do not move objects in pg_catalog as part of this, if an admin - * really wishes to do so, they can issue the individual ALTER - * commands directly. - * - * Also, explicitly avoid any shared tables, temp tables, or TOAST - * (TOAST will be moved with the main table). - */ - if (IsCatalogNamespace(relForm->relnamespace) || - relForm->relisshared || - isAnyTempNamespace(relForm->relnamespace) || - IsToastNamespace(relForm->relnamespace)) - continue; ++ Form_pg_attribute type_attr, ++ table_attr; ++ const char *type_attname, ++ *table_attname; - /* - * Callback to RangeVarGetRelidExtended() for TRUNCATE processing. - */ - static void - RangeVarCallbackForTruncate(const RangeVar *relation, - Oid relId, Oid oldRelId, void *arg) - { - HeapTuple tuple; - /* 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)) ++ /* 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); - /* Nothing to do if the relation was not found. */ - if (!OidIsValid(relId)) - return; - /* Check if we are only moving objects owned by certain roles */ - if (role_oids != NIL && !list_member_oid(role_oids, relForm->relowner)) - continue; ++ /* 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); + - /* - * Handle permissions-checking here since we are locking the tables - * and also to avoid doing a bunch of work only to fail part-way. Note - * that permissions will also be checked by AlterTableInternal(). - * - * Caller must be considered an owner on the table to move it. - */ - if (!object_ownercheck(RelationRelationId, relOid, GetUserId())) - aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relOid)), - NameStr(relForm->relname)); ++ /* 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))); + - if (stmt->nowait && - !ConditionalLockRelationOid(relOid, AccessExclusiveLock)) ++ /* 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_OBJECT_IN_USE), - errmsg("aborting because lock on relation \"%s.%s\" is not available", - get_namespace_name(relForm->relnamespace), - NameStr(relForm->relname)))); - else - LockRelationOid(relOid, AccessExclusiveLock); ++ (errcode(ERRCODE_DATATYPE_MISMATCH), ++ errmsg("table \"%s\" has different type for column \"%s\"", ++ RelationGetRelationName(rel), type_attname))); ++ } ++ ReleaseTupleDesc(typeTupleDesc); - tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relId)); - if (!HeapTupleIsValid(tuple)) /* should not happen */ - elog(ERROR, "cache lookup failed for relation %u", relId); - /* Add to our list of objects to move */ - relations = lappend_oid(relations, relOid); ++ /* 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); + - truncate_check_rel(relId, (Form_pg_class) GETSTRUCT(tuple)); - truncate_check_perms(relId, (Form_pg_class) GETSTRUCT(tuple)); ++ if (!table_attr->attisdropped) ++ ereport(ERROR, ++ (errcode(ERRCODE_DATATYPE_MISMATCH), ++ errmsg("table has extra column \"%s\"", ++ NameStr(table_attr->attname)))); + } - ReleaseSysCache(tuple); - } - table_endscan(scan); - table_close(rel, AccessShareLock); ++ /* 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); - /* - * 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; - 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)))); ++ /* 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); - /* Nothing to do if the relation was not found. */ - if (!OidIsValid(relId)) - return; - /* Everything is locked, loop through and move all of the relations. */ - foreach(l, relations) - { - List *cmds = NIL; - AlterTableCmd *cmd = makeNode(AlterTableCmd); ++ /* 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); - tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relId)); - if (!HeapTupleIsValid(tuple)) /* should not happen */ - elog(ERROR, "cache lookup failed for relation %u", relId); - cmd->subtype = AT_SetTableSpace; - cmd->name = stmt->new_tablespacename; ++ InvokeObjectPostAlterHook(RelationRelationId, relid, 0); - if (!object_ownercheck(RelationRelationId, relId, GetUserId())) - aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relId)), - relation->relname); - cmds = lappend(cmds, cmd); ++ heap_freetuple(classtuple); ++ table_close(relationRelation, RowExclusiveLock); - 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))); - EventTriggerAlterTableStart((Node *) stmt); - /* OID is set by AlterTableInternal */ - AlterTableInternal(lfirst_oid(l), cmds, false); - EventTriggerAlterTableEnd(); - } ++ ReleaseSysCache(typetuple); - ReleaseSysCache(tuple); - return new_tablespaceoid; ++ return typeobj; } +/* - * Common RangeVarGetRelid callback for rename, set schema, and alter table - * processing. ++ * ALTER TABLE NOT OF ++ * ++ * Detach a typed table from its originating type. Just clear reloftype and ++ * remove the dependency. + */ static void - RangeVarCallbackForAlterRelation(const RangeVar *rv, Oid relid, Oid oldrelid, - void *arg) -index_copy_data(Relation rel, RelFileLocator newrlocator) ++ATExecDropOf(Relation rel, LOCKMODE lockmode) { - Node *stmt = (Node *) arg; - ObjectType reltype; - SMgrRelation dstrel; ++ Oid relid = RelationGetRelid(rel); ++ Relation relationRelation; + 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)) - /* - * Since we copy the file directly without looking at the shared buffers, - * we'd better first flush out any pages of the source relation that are - * in shared buffers. We assume no new changes will be made while we are - * holding exclusive lock on the rel. - */ - FlushRelationBuffers(rel); ++ if (!OidIsValid(rel->rd_rel->reloftype)) + ereport(ERROR, - (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), - errmsg("permission denied: \"%s\" is a system catalog", - rv->relname))); - - /* - * 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. - */ - 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; - - else if (IsA(stmt, AlterTableStmt)) - reltype = ((AlterTableStmt *) stmt)->objtype; - else - { - elog(ERROR, "unrecognized node type: %d", (int) nodeTag(stmt)); - reltype = OBJECT_TABLE; /* placate compiler */ - } ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("\"%s\" is not a typed table", ++ RelationGetRelationName(rel)))); /* - * 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. - * 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(). ++ * 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. */ - 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))); - dstrel = RelationCreateStorage(newrlocator, rel->rd_rel->relpersistence, true); - 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))); - /* copy main fork */ - RelationCopyStorage(RelationGetSmgr(rel), dstrel, MAIN_FORKNUM, - rel->rd_rel->relpersistence); ++ drop_parent_dependency(relid, TypeRelationId, rel->rd_rel->reloftype, ++ DEPENDENCY_NORMAL); - /* - * 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"))); - /* copy those extra forks that exist */ - for (ForkNumber forkNum = MAIN_FORKNUM + 1; - forkNum <= MAX_FORKNUM; forkNum++) - { - if (smgrexists(RelationGetSmgr(rel), forkNum)) - { - smgrcreate(dstrel, forkNum, false); ++ /* 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); - /* - * 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."))); - /* - * WAL log creation if the relation is persistent, or this is the - * init fork of an unlogged relation. - */ - if (RelationIsPermanent(rel) || - (rel->rd_rel->relpersistence == RELPERSISTENCE_UNLOGGED && - forkNum == INIT_FORKNUM)) - log_smgrcreate(&newrlocator, forkNum); - RelationCopyStorage(RelationGetSmgr(rel), dstrel, forkNum, - rel->rd_rel->relpersistence); - } -- } ++ InvokeObjectPostAlterHook(RelationRelationId, relid, 0); - ReleaseSysCache(tuple); - /* drop old relation, and close new one */ - RelationDropStorage(rel); - smgrclose(dstrel); ++ heap_freetuple(tuple); ++ table_close(relationRelation, RowExclusiveLock); } /* - * Transform any expressions present in the partition key - * ALTER TABLE ENABLE/DISABLE TRIGGER ++ * relation_mark_replica_identity: Update a table's replica identity * - * Returns a transformed PartitionSpec. - * We just pass this off to trigger.c. ++ * 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 PartitionSpec * - transformPartitionSpec(Relation rel, PartitionSpec *partspec) + static void -ATExecEnableDisableTrigger(Relation rel, const char *trigname, - char fires_when, bool skip_system, bool recurse, - LOCKMODE lockmode) ++relation_mark_replica_identity(Relation rel, char ri_type, Oid indexOid, ++ bool is_internal) { - PartitionSpec *newspec; - ParseState *pstate; - ParseNamespaceItem *nsitem; - ListCell *l; - - newspec = makeNode(PartitionSpec); - - newspec->strategy = partspec->strategy; - newspec->partParams = NIL; - newspec->location = partspec->location; - EnableDisableTrigger(rel, trigname, InvalidOid, - fires_when, skip_system, recurse, - lockmode); ++ 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 valid number of columns for strategy */ - if (partspec->strategy == PARTITION_STRATEGY_LIST && - list_length(partspec->partParams) != 1) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("cannot use \"list\" partition strategy with more than one column"))); - InvokeObjectPostAlterHook(RelationRelationId, - RelationGetRelid(rel), 0); -} ++ /* ++ * 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); -/* - * 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); + /* - * Create a dummy ParseState and insert the target relation as its sole - * rangetable entry. We need a ParseState for transformExpr. ++ * Update the per-index indisreplident flags correctly. + */ - pstate = make_parsestate(NULL); - nsitem = addRangeTableEntryForRelation(pstate, rel, AccessShareLock, - NULL, false, true); - addNSItemToQuery(pstate, nsitem, true, true, true); - - /* take care of any partition expressions */ - foreach(l, partspec->partParams) ++ pg_index = table_open(IndexRelationId, RowExclusiveLock); ++ foreach(index, RelationGetIndexList(rel)) + { - PartitionElem *pelem = lfirst_node(PartitionElem, l); ++ Oid thisIndexOid = lfirst_oid(index); ++ bool dirty = false; + - InvokeObjectPostAlterHook(RelationRelationId, - RelationGetRelid(rel), 0); -} ++ 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 (pelem->expr) -/* - * ALTER TABLE INHERIT - * - * 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 void -ATPrepAddInherit(Relation child_rel) -{ - if (child_rel->rd_rel->reloftype) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot change inheritance of typed table"))); ++ if (thisIndexOid == indexOid) ++ { ++ /* Set the bit if not already set. */ ++ if (!pg_index_form->indisreplident) ++ { ++ dirty = true; ++ pg_index_form->indisreplident = true; ++ } ++ } ++ else + { - /* Copy, to avoid scribbling on the input */ - pelem = copyObject(pelem); ++ /* Unset the bit if set. */ ++ if (pg_index_form->indisreplident) ++ { ++ dirty = true; ++ pg_index_form->indisreplident = false; ++ } ++ } - /* Now do parse transformation of the expression */ - pelem->expr = transformExpr(pstate, pelem->expr, - EXPR_KIND_PARTITION_EXPRESSION); - if (child_rel->rd_rel->relispartition) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot change inheritance of a partition"))); ++ if (dirty) ++ { ++ CatalogTupleUpdate(pg_index, &pg_index_tuple->t_self, pg_index_tuple); ++ InvokeObjectPostAlterHookArg(IndexRelationId, thisIndexOid, 0, ++ InvalidOid, is_internal); + - /* we have to fix its collations too */ - assign_expr_collations(pstate, pelem->expr); ++ /* ++ * 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); + } - - newspec->partParams = lappend(newspec->partParams, pelem); ++ heap_freetuple(pg_index_tuple); + } - return newspec; - if (child_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot change inheritance of partitioned table"))); ++ table_close(pg_index, RowExclusiveLock); } /* - * Compute per-partition-column information from a list of PartitionElems. - * Expressions in the PartitionElems must be parse-analyzed already. - * Return the address of the new parent relation. ++ * ALTER TABLE REPLICA IDENTITY ... */ -static ObjectAddress -ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode) +static void - ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNumber *partattrs, - List **partexprs, Oid *partopclass, Oid *partcollation, - PartitionStrategy strategy) ++ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode) { - int attn; - ListCell *lc; - Oid am_oid; - 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); ++ Oid indexOid; ++ Relation indexRel; ++ int key; - attn = 0; - foreach(lc, partParams) - /* - * 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); ++ if (stmt->identity_type == REPLICA_IDENTITY_DEFAULT) + { - 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; - - Assert(expr != NULL); - atttype = exprType(expr); - attcollation = exprCollation(expr); - - /* - * 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); - - /* - * 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; - - /* - * 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); - } - - i = -1; - while ((i = bms_next_member(expr_attrs, i)) >= 0) - { - AttrNumber attno = i + FirstLowInvalidHeapAttributeNumber; - - Assert(attno != 0); - - /* - * 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"))); - - /* - * 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))); - } - - if (IsA(expr, Var) && - ((Var *) expr)->varattno > 0) - { - - /* - * 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); - - /* - * transformPartitionSpec() should have already rejected - * subqueries, aggregates, window functions, and SRFs, based - * on the EXPR_KIND_ for partition expressions. - */ - - /* - * 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); ++ 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); - /* - * 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"))); - /* Permanent rels cannot inherit from temporary ones */ - if (parent_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP && - child_rel->rd_rel->relpersistence != RELPERSISTENCE_TEMP) ++ /* Check that the index exists */ ++ indexOid = get_relname_relid(stmt->name, rel->rd_rel->relnamespace); ++ if (!OidIsValid(indexOid)) + ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot inherit from temporary relation \"%s\"", - RelationGetRelationName(parent_rel)))); ++ (errcode(ERRCODE_UNDEFINED_OBJECT), ++ errmsg("index \"%s\" for table \"%s\" does not exist", ++ stmt->name, RelationGetRelationName(rel)))); - /* - * 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"))); - } - } - /* 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"))); ++ indexRel = index_open(indexOid, ShareLock); - /* - * Apply collation override if any - */ - if (pelem->collation) - attcollation = get_collation_oid(pelem->collation, false); - /* Ditto for the child */ - if (RELATION_IS_OTHER_TEMP(child_rel)) ++ /* 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("cannot inherit to temporary relation of another session"))); ++ errmsg("\"%s\" is not an index for table \"%s\"", ++ RelationGetRelationName(indexRel), ++ RelationGetRelationName(rel)))); - /* - * 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)))); - } - /* Prevent partitioned tables from becoming inheritance parents */ - if (parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) ++ /* ++ * 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 inherit from partitioned table \"%s\"", - parent->relname))); - - /* Likewise for partitions */ - if (parent_rel->rd_rel->relispartition) ++ 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_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))) ++ (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_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) ++ (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("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."))); ++ errmsg("cannot use partial index \"%s\" as replica identity", ++ RelationGetRelationName(indexRel)))); - partcollation[attn] = attcollation; - /* OK to create inheritance */ - CreateInheritance(child_rel, parent_rel, false); ++ /* Check index for nullable columns. */ ++ for (key = 0; key < IndexRelationGetNumberOfKeyAttributes(indexRel); key++) ++ { ++ int16 attno = indexRel->rd_index->indkey.values[key]; ++ Form_pg_attribute attr; - ObjectAddressSet(address, RelationRelationId, - RelationGetRelid(parent_rel)); + /* - * Identify the appropriate operator class. For list and range - * partitioning, we use a btree operator class; hash partitioning uses - * a hash operator class. ++ * 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 (strategy == PARTITION_STRATEGY_HASH) - am_oid = HASH_AM_OID; - else - am_oid = BTREE_AM_OID; ++ 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))); - if (!pelem->opclass) - { - partopclass[attn] = GetDefaultOpClass(atttype, am_oid); - /* keep our lock on the parent relation until commit */ - table_close(parent_rel, NoLock); ++ 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)))); ++ } - 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."))); - } - } - else - partopclass[attn] = ResolveOpClass(pelem->opclass, - atttype, - am_oid == HASH_AM_OID ? "hash" : "btree", - am_oid); - return address; ++ /* This index is suitable for use as a replica identity. Mark it. */ ++ relation_mark_replica_identity(rel, stmt->identity_type, indexOid, true); + - attn++; - } ++ index_close(indexRel, NoLock); } /* - * PartConstraintImpliedByRelConstraint - * Do scanrel's existing constraints imply the partition constraint? - * CreateInheritance - * Catalog manipulation portion of creating inheritance between a child - * table and a parent table. -- * - * "Existing constraints" include its check constraints and column-level - * not-null constraints. partConstraint describes the partition constraint, - * in implicit-AND form. - * Common to ATExecAddInherit() and ATExecAttachPartition(). ++ * ALTER TABLE ENABLE/DISABLE ROW LEVEL SECURITY */ - bool - PartConstraintImpliedByRelConstraint(Relation scanrel, - List *partConstraint) -void -CreateInheritance(Relation child_rel, Relation parent_rel, bool ispartition) ++static void ++ATExecSetRowSecurity(Relation rel, bool rls) { - List *existConstraint = NIL; - TupleConstr *constr = RelationGetDescr(scanrel)->constr; - int i; - 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)))); ++ Relation pg_class; ++ Oid relid; ++ HeapTuple tuple; - if (constr && constr->has_not_null) - { - int natts = scanrel->rd_att->natts; - if (inh->inhseqno > inhseqno) - inhseqno = inh->inhseqno; - } - systable_endscan(scan); ++ relid = RelationGetRelid(rel); - for (i = 1; i <= natts; i++) - { - CompactAttribute *att = TupleDescCompactAttr(scanrel->rd_att, i - 1); - /* Match up the columns and bump attinhcount as needed */ - MergeAttributesIntoExisting(child_rel, parent_rel, ispartition); ++ /* Pull the record for this relation and update it */ ++ pg_class = table_open(RelationRelationId, RowExclusiveLock); - /* 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); - /* Match up the constraints and bump coninhcount as needed */ - MergeConstraintsIntoExisting(child_rel, parent_rel); ++ tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid)); - ntest->arg = (Expr *) makeVar(1, - i, - wholeatt->atttypid, - wholeatt->atttypmod, - wholeatt->attcollation, - 0); - ntest->nulltesttype = IS_NOT_NULL; - /* - * 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); ++ if (!HeapTupleIsValid(tuple)) ++ elog(ERROR, "cache lookup failed for relation %u", relid); - /* - * 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); - } - } - } - /* Now we're done with pg_inherits */ - table_close(catalogRelation, RowExclusiveLock); ++ ((Form_pg_class) GETSTRUCT(tuple))->relrowsecurity = rls; ++ CatalogTupleUpdate(pg_class, &tuple->t_self, tuple); ++ ++ InvokeObjectPostAlterHook(RelationRelationId, ++ RelationGetRelid(rel), 0); + - return ConstraintImpliedByRelConstraint(scanrel, partConstraint, existConstraint); ++ table_close(pg_class, RowExclusiveLock); ++ heap_freetuple(tuple); } /* - * 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. - * Obtain the source-text form of the constraint expression for a check - * constraint, given its pg_constraint tuple ++ * ALTER TABLE FORCE/NO FORCE ROW LEVEL SECURITY */ - bool - ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint, List *provenConstraint) -static char * -decompile_conbin(HeapTuple contup, TupleDesc tupdesc) ++static void ++ATExecForceNoForceRowSecurity(Relation rel, bool force_rls) { - List *existConstraint = list_copy(provenConstraint); - TupleConstr *constr = RelationGetDescr(scanrel)->constr; - int num_check, - i; - Form_pg_constraint con; - bool isnull; - Datum attr; - Datum expr; ++ Relation pg_class; ++ Oid relid; ++ HeapTuple tuple; - num_check = (constr != NULL) ? constr->num_check : 0; - for (i = 0; i < num_check; i++) - { - Node *cexpr; - 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); ++ relid = RelationGetRelid(rel); - /* - * If this constraint hasn't been fully validated yet, we must ignore - * it here. - */ - if (!constr->check[i].ccvalid) - continue; - expr = DirectFunctionCall2(pg_get_expr, attr, - ObjectIdGetDatum(con->conrelid)); - return TextDatumGetCString(expr); -} ++ pg_class = table_open(RelationRelationId, RowExclusiveLock); - /* - * NOT ENFORCED constraints are always marked as invalid, which should - * have been ignored. - */ - Assert(constr->check[i].ccenforced); -/* - * 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); ++ tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid)); - cexpr = stringToNode(constr->check[i].ccbin); - if (acon->condeferrable != bcon->condeferrable || - acon->condeferred != bcon->condeferred || - strcmp(decompile_conbin(a, tupleDesc), - decompile_conbin(b, tupleDesc)) != 0) - return false; - else - return true; ++ if (!HeapTupleIsValid(tuple)) ++ elog(ERROR, "cache lookup failed for relation %u", relid); + - /* - * 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); ++ ((Form_pg_class) GETSTRUCT(tuple))->relforcerowsecurity = force_rls; ++ CatalogTupleUpdate(pg_class, &tuple->t_self, tuple); + - existConstraint = list_concat(existConstraint, - make_ands_implicit((Expr *) cexpr)); - } ++ InvokeObjectPostAlterHook(RelationRelationId, ++ RelationGetRelid(rel), 0); + - /* - * 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); ++ table_close(pg_class, RowExclusiveLock); ++ heap_freetuple(tuple); } /* - * QueuePartitionConstraintValidation - * Check columns in child table match up with columns in parent, and increment - * their attinhcount. -- * - * Add an entry to wqueue to have the given partition constraint validated by - * Phase 3, for the given relation, and all its children. - * Called by CreateInheritance -- * - * 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. - * 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. ++ * ALTER FOREIGN TABLE OPTIONS (...) */ static void - QueuePartitionConstraintValidation(List **wqueue, Relation scanrel, - List *partConstraint, - bool validate_default) -MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispartition) ++ATExecGenericOptions(Relation rel, List *options) { - /* - * Based on the table's existing constraints, determine whether or not we - * may skip scanning the table. - */ - 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)))); - Relation attrrel; - TupleDesc parent_desc; ++ 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; + - attrrel = table_open(AttributeRelationId, RowExclusiveLock); - parent_desc = RelationGetDescr(parent_rel); ++ if (options == NIL) + return; - } + - 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; ++ ftrel = table_open(ForeignTableRelationId, RowExclusiveLock); + - /* Ignore dropped columns in the parent. */ - if (parent_att->attisdropped) - continue; ++ 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); + - /* 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); ++ memset(repl_val, 0, sizeof(repl_val)); ++ memset(repl_null, false, sizeof(repl_null)); ++ memset(repl_repl, false, sizeof(repl_repl)); + - 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))); ++ /* Extract the current options */ ++ datum = SysCacheGetAttr(FOREIGNTABLEREL, ++ tuple, ++ Anum_pg_foreign_table_ftoptions, ++ &isnull); ++ if (isnull) ++ datum = PointerGetDatum(NULL); + - 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))); ++ /* Transform the options */ ++ datum = transformGenericOptions(ForeignTableRelationId, ++ datum, ++ options, ++ fdw->fdwvalidator); + - /* - * 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; ++ if (DatumGetPointer(datum) != NULL) ++ repl_val[Anum_pg_foreign_table_ftoptions - 1] = datum; ++ else ++ repl_null[Anum_pg_foreign_table_ftoptions - 1] = true; + - 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))); - } ++ repl_repl[Anum_pg_foreign_table_ftoptions - 1] = true; + - /* - * 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))); ++ /* Everything looks good - update the tuple */ + - 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"))); ++ tuple = heap_modify_tuple(tuple, RelationGetDescr(ftrel), ++ repl_val, repl_null, repl_repl); + - /* - * 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; ++ CatalogTupleUpdate(ftrel, &tuple->t_self, tuple); - /* - * 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")); + /* - * Constraints proved insufficient. For plain relations, queue a - * validation item now; for partitioned tables, recurse to process each - * partition. ++ * Invalidate relcache so that all sessions will refresh any cached plans ++ * that might depend on the old options. + */ - if (scanrel->rd_rel->relkind == RELKIND_RELATION) - { - AlteredTableInfo *tab; - - /* 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; ++ CacheInvalidateRelcache(rel); - for (i = 0; i < partdesc->nparts; i++) - { - Relation part_rel; - List *thisPartConstraint; - /* - * 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; - } ++ InvokeObjectPostAlterHook(ForeignTableRelationId, ++ RelationGetRelid(rel), 0); - /* - * This is the minimum lock we need to prevent deadlocks. - */ - part_rel = table_open(partdesc->oids[i], AccessExclusiveLock); - 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(ftrel, RowExclusiveLock); - /* - * Adjust the constraint for scanrel so that it matches this - * partition's attribute numbers. - */ - 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 */ - } - } - table_close(attrrel, RowExclusiveLock); ++ heap_freetuple(tuple); +} + +/* ++<<<<<<< ours + * attachPartitionTable: attach a new partition to the partitioned table + * + * 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. + */ +static void +attachPartitionTable(List **wqueue, Relation rel, Relation attachrel, PartitionBoundSpec *bound) +{ + /* + * Create an inheritance; the relevant checks are performed inside the + * function. + */ + CreateInheritance(attachrel, rel, true); + + /* Update the pg_class entry. */ + StorePartitionBound(attachrel, rel, bound); + + /* Ensure there exists a correct set of indexes in the partition. */ + AttachPartitionEnsureIndexes(wqueue, rel, attachrel); + + /* and triggers */ + CloneRowTriggersToPartition(rel, attachrel); + + /* + * Clone foreign key constraints. Callee is responsible for setting up + * for phase 3 constraint verification. + */ + CloneForeignKeyConstraints(wqueue, rel, attachrel); } /* - * 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. + * ALTER TABLE ATTACH PARTITION FOR VALUES ++======= ++ * ALTER TABLE ALTER COLUMN SET COMPRESSION ++>>>>>>> theirs * - * Return the address of the newly attached partition. - * XXX See MergeWithExistingConstraint too if you change this code. ++ * Return value is the address of the modified column */ -static void -MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel) +static ObjectAddress - ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd, - AlterTableUtilityContext *context) ++ATExecSetCompression(Relation rel, ++ const char *column, ++ Node *newValue, ++ LOCKMODE lockmode) { - Relation attachrel, - catalog; - List *attachrel_children; - List *partConstraint; - SysScanDesc scan; - ScanKeyData skey; - AttrNumber attno; - int natts; - TupleDesc tupleDesc; - Relation constraintrel; - SysScanDesc parent_scan; - ScanKeyData parent_key; - HeapTuple parent_tuple; - Oid parent_relid = RelationGetRelid(parent_rel); - AttrMap *attmap; ++ Relation attrel; ++ HeapTuple tuple; ++ Form_pg_attribute atttableform; ++ AttrNumber attnum; ++ char *compression; ++ char cmethod; + ObjectAddress address; - const char *trigger_name; - Oid defaultPartOid; - List *partBoundConstraint; - ParseState *pstate = make_parsestate(NULL); - pstate->p_sourcetext = context->queryString; - constraintrel = table_open(ConstraintRelationId, RowExclusiveLock); ++ compression = strVal(newValue); - /* - * 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); - /* 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); ++ attrel = table_open(AttributeRelationId, RowExclusiveLock); - attachrel = table_openrv(cmd->name, AccessExclusiveLock); - attmap = build_attrmap_by_name(RelationGetDescr(parent_rel), - RelationGetDescr(child_rel), - true); ++ /* 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)))); - /* - * XXX I think it'd be a good idea to grab locks on all tables referenced - * by FKs at this point also. - */ - 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; ++ /* 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))); - if (parent_con->contype != CONSTRAINT_CHECK && - parent_con->contype != CONSTRAINT_NOTNULL) - continue; + /* - * Must be owner of both parent and source table -- parent was checked by - * ATSimplePermissions call in ATPrepCmd ++ * Check that column type is compressible, then get the attribute ++ * compression method code + */ - ATSimplePermissions(AT_AttachPartition, attachrel, - ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); ++ cmethod = GetAttributeCompression(atttableform->atttypid, compression); - /* 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 the parent's constraint is marked NO INHERIT, it's not inherited */ - if (parent_con->connoinherit) - continue; ++ /* update pg_attribute entry */ ++ atttableform->attcompression = cmethod; ++ CatalogTupleUpdate(attrel, &tuple->t_self, tuple); - if (OidIsValid(attachrel->rd_rel->reloftype)) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot attach a typed table as partition"))); - if (parent_con->contype == CONSTRAINT_NOTNULL) - parent_attno = extractNotNullColumn(parent_tuple); - else - parent_attno = InvalidAttrNumber; ++ InvokeObjectPostAlterHook(RelationRelationId, ++ RelationGetRelid(rel), ++ attnum); - /* 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); + /* - * Table being attached should not already be part of inheritance; either - * as a child table... ++ * Apply the change to indexes as well (only for simple index columns, ++ * matching behavior of index.c ConstructTupleDescriptor()). + */ - 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); ++ SetIndexStorageProperties(rel, attrel, attnum, ++ false, 0, ++ true, cmethod, ++ lockmode); - /* ...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) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot attach inheritance parent as partition"))); - systable_endscan(scan); - table_close(catalog, AccessShareLock); - while (HeapTupleIsValid(child_tuple = systable_getnext(child_scan))) - { - Form_pg_constraint child_con = (Form_pg_constraint) GETSTRUCT(child_tuple); - HeapTuple child_copy; ++ heap_freetuple(tuple); - /* - * 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)))); - if (child_con->contype != parent_con->contype) - continue; ++ table_close(attrel, RowExclusiveLock); - /* 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) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot attach a temporary relation as partition of permanent relation \"%s\"", - RelationGetRelationName(rel)))); - /* - * 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; ++ /* make changes visible */ ++ CommandCounterIncrement(); - /* 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) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot attach a permanent relation as partition of temporary relation \"%s\"", - RelationGetRelationName(rel)))); - parent_attr = TupleDescAttr(parent_rel->rd_att, parent_attno - 1); - child_attno = extractNotNullColumn(child_tuple); - if (parent_attno != attmap->attnums[child_attno - 1]) - continue; ++ ObjectAddressSubSet(address, RelationRelationId, ++ RelationGetRelid(rel), attnum); ++ return address; ++} - /* If the parent is temp, it must belong to this session */ - if (RELATION_IS_OTHER_TEMP(rel)) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot attach as partition of temporary relation of another session"))); - 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"); - } - /* Ditto for the partition */ - if (RELATION_IS_OTHER_TEMP(attachrel)) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot attach temporary relation of another session as partition"))); - 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)))); ++/* ++ * 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]; - /* - * 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)))); + /* - * Check if attachrel has any identity columns or any columns that aren't - * in the parent. ++ * 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. + */ - tupleDesc = RelationGetDescr(attachrel); - natts = tupleDesc->natts; - for (attno = 1; attno <= natts; attno++) ++ switch (rel->rd_rel->relpersistence) + { - 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))) ++ case RELPERSISTENCE_TEMP: + ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("table \"%s\" contains column \"%s\" not found in parent \"%s\"", - RelationGetRelationName(attachrel), attributeName, ++ (errcode(ERRCODE_INVALID_TABLE_DEFINITION), ++ errmsg("cannot change logged status of table \"%s\" because it is temporary", + RelationGetRelationName(rel)), - errdetail("The new partition may contain only the columns present in parent."))); ++ errtable(rel))); ++ break; ++ case RELPERSISTENCE_PERMANENT: ++ if (toLogged) ++ /* nothing to do */ ++ return; ++ break; ++ case RELPERSISTENCE_UNLOGGED: ++ if (!toLogged) ++ /* nothing to do */ ++ return; ++ break; + } - /* - * 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)))); + /* - * If child_rel has row-level triggers with transition tables, we - * currently don't allow it to become a partition. See also prohibitions - * in ATExecAddInherit() and CreateTrigger(). ++ * Check that the table is not part of any publication when changing to ++ * UNLOGGED, as UNLOGGED tables can't be published. + */ - trigger_name = FindTriggerIncompatibleWithInheritance(attachrel->trigdesc); - if (trigger_name != NULL) ++ if (!toLogged && ++ GetRelationPublications(RelationGetRelid(rel)) != NIL) + ereport(ERROR, ++<<<<<<< ours + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("trigger \"%s\" prevents table \"%s\" from becoming a partition", + trigger_name, RelationGetRelationName(attachrel)), + errdetail("ROW triggers with transition tables are not supported on partitions."))); - /* - * 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)))); + /* + * Check that the new partition's bound is valid and does not overlap any + * of existing partitions of the parent - note that it does not return on + * error. + */ + check_new_partition_bound(RelationGetRelationName(attachrel), rel, + cmd->bound, pstate); - /* - * 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); + attachPartitionTable(wqueue, rel, attachrel, cmd->bound); + + /* + * Generate a partition constraint from the partition bound specification. + * If the parent itself is a partition, make sure to include its + * constraint as well. - */ - partBoundConstraint = get_qual_from_partbound(rel, cmd->bound); ++======= ++ (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."))); + + /* - * Use list_concat_copy() to avoid modifying partBoundConstraint in place, - * since it's needed later to construct the constraint expression for - * validating against the default partition, if any. ++ * Check existing foreign key constraints to preserve the invariant that ++ * permanent tables cannot reference unlogged ones. Self-referencing ++ * foreign keys can safely be ignored. ++>>>>>>> theirs + */ - partConstraint = list_concat_copy(partBoundConstraint, - RelationGetPartitionQual(rel)); - - /* Skip validation if there are no constraints to validate. */ - if (partConstraint) - { - /* - * Run the partition quals through const-simplification similar to - * check constraints. We skip canonicalize_qual, though, because - * partition quals should be in canonical form already. - */ - partConstraint = - (List *) eval_const_expressions(NULL, - (Node *) partConstraint); - - /* XXX this sure looks wrong */ - partConstraint = list_make1(make_ands_explicit(partConstraint)); - - /* - * Adjust the generated constraint to match this partition's attribute - * numbers. - */ - partConstraint = map_partition_varattnos(partConstraint, 1, attachrel, - rel); - - /* Validate partition constraints against the table being attached. */ - QueuePartitionConstraintValidation(wqueue, attachrel, partConstraint, - false); - } ++ pg_constraint = table_open(ConstraintRelationId, AccessShareLock); + + /* - * If we're attaching a partition other than the default partition and a - * default one exists, then that partition's partition constraint changes, - * so add an entry to the work queue to validate it, too. (We must not do - * this when the partition being attached is the default one; we already - * did it above!) ++ * Scan conrelid if changing to permanent, else confrelid. This also ++ * determines whether a useful index exists. + */ - if (OidIsValid(defaultPartOid)) - { - Relation defaultrel; - List *defPartConstraint; ++ 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); - Assert(!cmd->bound->is_default); - if (pg_add_s16_overflow(child_con->coninhcount, 1, - &child_con->coninhcount)) - ereport(ERROR, - errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("too many inheritance parents")); ++ while (HeapTupleIsValid(tuple = systable_getnext(scan))) ++ { ++ Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(tuple); - /* we already hold a lock on the default partition */ - defaultrel = table_open(defaultPartOid, NoLock); - defPartConstraint = - get_proposed_default_constraint(partBoundConstraint); - /* - * 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; - } ++ if (con->contype == CONSTRAINT_FOREIGN) ++ { ++ Oid foreignrelid; ++ Relation foreignrel; - /* - * Map the Vars in the constraint expression from rel's attnos to - * defaultrel's. - */ - defPartConstraint = - map_partition_varattnos(defPartConstraint, - 1, defaultrel, rel); - QueuePartitionConstraintValidation(wqueue, defaultrel, - defPartConstraint, true); - CatalogTupleUpdate(constraintrel, &child_copy->t_self, child_copy); - heap_freetuple(child_copy); ++ /* the opposite end of what we used as scankey */ ++ foreignrelid = toLogged ? con->confrelid : con->conrelid; - /* keep our lock until commit. */ - table_close(defaultrel, NoLock); - } - found = true; - break; - } ++ /* ignore if self-referencing */ ++ if (RelationGetRelid(rel) == foreignrelid) ++ continue; - ObjectAddressSet(address, RelationRelationId, RelationGetRelid(attachrel)); - systable_endscan(child_scan); ++ foreignrel = relation_open(foreignrelid, AccessShareLock); - /* - * If the partition we just attached is partitioned itself, invalidate - * relcache for all descendent partitions too to ensure that their - * rd_partcheck expression trees are rebuilt; partitions already locked at - * the beginning of this function. - */ - if (attachrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) - { - ListCell *l; - 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))); ++ 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)))); ++ } - foreach(l, attachrel_children) - { - CacheInvalidateRelcacheByRelid(lfirst_oid(l)); - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("child table is missing constraint \"%s\"", - NameStr(parent_con->conname)))); ++ relation_close(foreignrel, AccessShareLock); } } - /* keep our lock until commit */ - table_close(attachrel, NoLock); - systable_endscan(parent_scan); - table_close(constraintrel, RowExclusiveLock); ++ systable_endscan(scan); + - return address; ++ 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; } /* - * AttachPartitionEnsureIndexes - * subroutine for ATExecAttachPartition to create/match indexes - * ALTER TABLE NO INHERIT -- * - * Enforce the indexing rule for partitioned tables during ALTER TABLE / ATTACH - * PARTITION: every partition must have an index attached to each index on the - * partitioned table. - * Return value is the address of the relation that is no longer parent. ++ * Execute ALTER TABLE SET SCHEMA */ - static void - AttachPartitionEnsureIndexes(List **wqueue, Relation rel, Relation attachrel) -static ObjectAddress -ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode) ++ObjectAddress ++AlterTableNamespace(AlterObjectSchemaStmt *stmt, Oid *oldschema) { - List *idxes; - List *attachRelIdxs; - Relation *attachrelIdxRels; - IndexInfo **attachInfos; - ListCell *cell; - MemoryContext cxt; - MemoryContext oldcxt; - - cxt = AllocSetContextCreate(CurrentMemoryContext, - "AttachPartitionEnsureIndexes", - ALLOCSET_DEFAULT_SIZES); - oldcxt = MemoryContextSwitchTo(cxt); - ObjectAddress address; - Relation parent_rel; ++ Relation rel; ++ Oid relid; ++ Oid oldNspOid; ++ Oid nspOid; ++ RangeVar *newrv; ++ ObjectAddresses *objsMoved; ++ ObjectAddress myself; - if (rel->rd_rel->relispartition) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot change inheritance of a partition"))); ++<<<<<<< ours + idxes = RelationGetIndexList(rel); + attachRelIdxs = RelationGetIndexList(attachrel); + attachrelIdxRels = palloc_array(Relation, list_length(attachRelIdxs)); + attachInfos = palloc_array(IndexInfo *, list_length(attachRelIdxs)); ++======= ++ relid = RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock, ++ stmt->missing_ok ? RVR_MISSING_OK : 0, ++ RangeVarCallbackForAlterRelation, ++ stmt); ++>>>>>>> theirs - /* Build arrays of all existing indexes and their IndexInfos */ - foreach_oid(cldIdxId, attachRelIdxs) - /* - * 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); ++ if (!OidIsValid(relid)) + { - int i = foreach_current_index(cldIdxId); - - attachrelIdxRels[i] = index_open(cldIdxId, AccessShareLock); - attachInfos[i] = BuildIndexInfo(attachrelIdxRels[i]); ++ ereport(NOTICE, ++ (errmsg("relation \"%s\" does not exist, skipping", ++ stmt->relation->relname))); ++ return InvalidObjectAddress; + } -- /* - * If we're attaching a foreign table, we must fail if any of the indexes - * is a constraint index; otherwise, there's nothing to do here. Do this - * before starting work, to avoid wasting the effort of building a few - * non-unique indexes before coming across a unique one. - * We don't bother to check ownership of the parent table --- ownership of - * the child is presumed enough rights. -- */ - if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) - { - foreach(cell, idxes) - { - Oid idx = lfirst_oid(cell); - Relation idxRel = index_open(idx, AccessShareLock); - - if (idxRel->rd_index->indisunique || - idxRel->rd_index->indisprimary) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot attach foreign table \"%s\" as partition of partitioned table \"%s\"", - RelationGetRelationName(attachrel), - RelationGetRelationName(rel)), - errdetail("Partitioned table \"%s\" contains unique indexes.", - RelationGetRelationName(rel)))); - index_close(idxRel, AccessShareLock); - } ++ rel = relation_open(relid, NoLock); - goto out; - } - /* Off to RemoveInheritance() where most of the work happens */ - RemoveInheritance(rel, parent_rel, false); ++ oldNspOid = RelationGetNamespace(rel); - /* - * For each index on the partitioned table, find a matching one in the - * partition-to-be; if one is not found, create one. - */ - foreach(cell, idxes) - ObjectAddressSet(address, RelationRelationId, - RelationGetRelid(parent_rel)); ++ /* If it's an owned sequence, disallow moving it by itself. */ ++ if (rel->rd_rel->relkind == RELKIND_SEQUENCE) + { - Oid idx = lfirst_oid(cell); - Relation idxRel = index_open(idx, AccessShareLock); - IndexInfo *info; - AttrMap *attmap; - bool found = false; - Oid constraintOid; - - /* - * Ignore indexes in the partitioned table other than partitioned - * indexes. - */ - if (idxRel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX) - { - index_close(idxRel, AccessShareLock); - continue; - } - - /* construct an indexinfo to compare existing indexes against */ - info = BuildIndexInfo(idxRel); - attmap = build_attrmap_by_name(RelationGetDescr(attachrel), - RelationGetDescr(rel), - false); - constraintOid = get_relation_idx_constraint_oid(RelationGetRelid(rel), idx); - - /* - * Scan the list of existing indexes in the partition-to-be, and mark - * the first matching, valid, unattached one we find, if any, as - * partition of the parent index. If we find one, we're done. - */ - for (int i = 0; i < list_length(attachRelIdxs); i++) - { - Oid cldIdxId = RelationGetRelid(attachrelIdxRels[i]); - Oid cldConstrOid = InvalidOid; - - /* does this index have a parent? if so, can't use it */ - if (attachrelIdxRels[i]->rd_rel->relispartition) - continue; - - /* If this index is invalid, can't use it */ - if (!attachrelIdxRels[i]->rd_index->indisvalid) - continue; ++ Oid tableId; ++ int32 colId; - if (CompareIndexInfo(attachInfos[i], info, - attachrelIdxRels[i]->rd_indcollation, - idxRel->rd_indcollation, - attachrelIdxRels[i]->rd_opfamily, - idxRel->rd_opfamily, - attmap)) - { - /* - * If this index is being created in the parent because of a - * constraint, then the child needs to have a constraint also, - * so look for one. If there is no such constraint, this - * index is no good, so keep looking. - */ - if (OidIsValid(constraintOid)) - { - cldConstrOid = - get_relation_idx_constraint_oid(RelationGetRelid(attachrel), - cldIdxId); - /* no dice */ - if (!OidIsValid(cldConstrOid)) - continue; - /* keep our lock on the parent relation until commit */ - table_close(parent_rel, NoLock); ++ 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)))); ++ } - /* Ensure they're both the same type of constraint */ - if (get_constraint_type(constraintOid) != - get_constraint_type(cldConstrOid)) - continue; - } - return address; ++ /* Get and lock schema OID and check its permissions. */ ++ newrv = makeRangeVar(stmt->newschema, RelationGetRelationName(rel), -1); ++ nspOid = RangeVarGetAndCheckCreationNamespace(newrv, NoLock, NULL); + - /* bingo. */ - IndexSetParentIndex(attachrelIdxRels[i], idx); - if (OidIsValid(constraintOid)) - ConstraintSetParentConstraint(cldConstrOid, constraintOid, - RelationGetRelid(attachrel)); - found = true; ++ /* common checks on switching namespaces */ ++ CheckSetNamespace(oldNspOid, nspOid); + - CommandCounterIncrement(); - break; - } - } ++ objsMoved = new_object_addresses(); ++ AlterTableNamespaceInternal(rel, oldNspOid, nspOid, objsMoved); ++ free_object_addresses(objsMoved); + - /* - * If no suitable index was found in the partition-to-be, create one - * now. Note that if this is a PK, not-null constraints must already - * exist. - */ - if (!found) - { - IndexStmt *stmt; - Oid conOid; ++ ObjectAddressSet(myself, RelationRelationId, relid); + ++<<<<<<< ours + stmt = generateClonedIndexStmt(NULL, + idxRel, attmap, + &conOid); + DefineIndex(NULL, + RelationGetRelid(attachrel), stmt, InvalidOid, + RelationGetRelid(idxRel), + conOid, + -1, + true, false, false, false, false); + } ++======= ++ if (oldschema) ++ *oldschema = oldNspOid; ++>>>>>>> theirs + - index_close(idxRel, AccessShareLock); - } ++ /* close rel, but keep lock until commit */ ++ relation_close(rel, NoLock); + - out: - /* Clean up. */ - for (int i = 0; i < list_length(attachRelIdxs); i++) - index_close(attachrelIdxRels[i], AccessShareLock); - MemoryContextSwitchTo(oldcxt); - MemoryContextDelete(cxt); ++ return myself; } /* - * CloneRowTriggersToPartition - * subroutine for ATExecAttachPartition/DefineRelation to create row - * triggers on partitions - * 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(). ++ * 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. */ - static void - CloneRowTriggersToPartition(Relation parent, Relation partition) + void -RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached) ++AlterTableNamespaceInternal(Relation rel, Oid oldNspOid, Oid nspOid, ++ ObjectAddresses *objsMoved) { - Relation pg_trigger; - ScanKeyData key; - Relation catalogRelation; -- SysScanDesc scan; - HeapTuple tuple; - MemoryContext perTupCxt; - - ScanKeyInit(&key, Anum_pg_trigger_tgrelid, BTEqualStrategyNumber, - F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(parent))); - pg_trigger = table_open(TriggerRelationId, RowExclusiveLock); - scan = systable_beginscan(pg_trigger, TriggerRelidNameIndexId, - true, NULL, 1, &key); - ScanKeyData key[3]; - HeapTuple attributeTuple, - constraintTuple; - AttrMap *attmap; - List *connames; - List *nncolumns; - bool found; - bool is_partitioning; ++ Relation classRel; - perTupCxt = AllocSetContextCreate(CurrentMemoryContext, - "clone trig", ALLOCSET_SMALL_SIZES); - is_partitioning = (parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); ++ Assert(objsMoved != NULL); - while (HeapTupleIsValid(tuple = systable_getnext(scan))) - found = DeleteInheritsTuple(RelationGetRelid(child_rel), - RelationGetRelid(parent_rel), - expect_detached, - RelationGetRelationName(child_rel)); - if (!found) -- { - Form_pg_trigger trigForm = (Form_pg_trigger) GETSTRUCT(tuple); - CreateTrigStmt *trigStmt; - Node *qual = NULL; - Datum value; - bool isnull; - List *cols = NIL; - List *trigargs = NIL; - MemoryContext oldcxt; - 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)))); - } ++ /* OK, modify the pg_class row and pg_depend entry */ ++ classRel = table_open(RelationRelationId, RowExclusiveLock); - /* - * Ignore statement-level triggers; those are not cloned. - */ - if (!TRIGGER_FOR_ROW(trigForm->tgtype)) - continue; - /* - * 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); ++ AlterRelationNamespaceInternal(classRel, RelationGetRelid(rel), oldNspOid, ++ nspOid, true, objsMoved); + - /* - * Don't clone internal triggers, because the constraint cloning code - * will. - */ - if (trigForm->tgisinternal) - continue; ++ /* 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); + - /* - * Complain if we find an unexpected trigger type. - */ - if (!TRIGGER_FOR_BEFORE(trigForm->tgtype) && - !TRIGGER_FOR_AFTER(trigForm->tgtype)) - elog(ERROR, "unexpected trigger \"%s\" found", - NameStr(trigForm->tgname)); ++ /* Fix other dependent stuff */ ++ AlterIndexNamespaces(classRel, rel, oldNspOid, nspOid, objsMoved); ++ AlterSeqNamespaces(classRel, rel, oldNspOid, nspOid, ++ objsMoved, AccessExclusiveLock); ++ AlterConstraintNamespaces(RelationGetRelid(rel), oldNspOid, nspOid, ++ false, objsMoved); - /* Use short-lived context for CREATE TRIGGER */ - oldcxt = MemoryContextSwitchTo(perTupCxt); - /* Ignore if dropped or not inherited */ - if (att->attisdropped) - continue; - if (att->attinhcount <= 0) - continue; ++ table_close(classRel, RowExclusiveLock); ++} - /* - * If there is a WHEN clause, generate a 'cooked' version of it that's - * appropriate for the partition. - */ - value = heap_getattr(tuple, Anum_pg_trigger_tgqual, - RelationGetDescr(pg_trigger), &isnull); - if (!isnull) - if (SearchSysCacheExistsAttName(RelationGetRelid(parent_rel), - NameStr(att->attname))) -- { - qual = stringToNode(TextDatumGetCString(value)); - qual = (Node *) map_partition_varattnos((List *) qual, PRS2_OLD_VARNO, - partition, parent); - qual = (Node *) map_partition_varattnos((List *) qual, PRS2_NEW_VARNO, - partition, parent); - } - /* Decrement inhcount and possibly set islocal to true */ - HeapTuple copyTuple = heap_copytuple(attributeTuple); - Form_pg_attribute copy_att = (Form_pg_attribute) GETSTRUCT(copyTuple); ++/* ++ * 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; - /* - * If there is a column list, transform it to a list of column names. - * Note we don't need to map this list in any way ... - */ - if (trigForm->tgattr.dim1 > 0) - { - int i; - copy_att->attinhcount--; - if (copy_att->attinhcount == 0) - copy_att->attislocal = true; ++ /* 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); - for (i = 0; i < trigForm->tgattr.dim1; i++) - { - Form_pg_attribute col; - CatalogTupleUpdate(catalogRelation, ©Tuple->t_self, copyTuple); - heap_freetuple(copyTuple); - } - } - systable_endscan(scan); - table_close(catalogRelation, RowExclusiveLock); ++ Assert(classForm->relnamespace == oldNspOid); + - col = TupleDescAttr(parent->rd_att, - trigForm->tgattr.values[i] - 1); - cols = lappend(cols, - makeString(pstrdup(NameStr(col->attname)))); - } - } ++ thisobj.classId = RelationRelationId; ++ thisobj.objectId = relOid; ++ thisobj.objectSubId = 0; - /* Reconstruct trigger arguments list. */ - if (trigForm->tgnargs > 0) - { - char *p; + /* - * 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. ++ * 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. + */ - attmap = build_attrmap_by_name(RelationGetDescr(child_rel), - RelationGetDescr(parent_rel), - false); ++ already_done = object_address_present(&thisobj, objsMoved); ++ if (!already_done && oldNspOid != newNspOid) ++ { ++ ItemPointerData otid = classTup->t_self; - value = heap_getattr(tuple, Anum_pg_trigger_tgargs, - RelationGetDescr(pg_trigger), &isnull); - if (isnull) - elog(ERROR, "tgargs is null for trigger \"%s\" in partition \"%s\"", - NameStr(trigForm->tgname), RelationGetRelationName(partition)); - 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); ++ /* 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)))); - p = (char *) VARDATA_ANY(DatumGetByteaPP(value)); - connames = NIL; - nncolumns = NIL; ++ /* classTup is a copy, so OK to scribble on */ ++ classForm->relnamespace = newNspOid; - for (int i = 0; i < trigForm->tgnargs; i++) - { - trigargs = lappend(trigargs, makeString(pstrdup(p))); - p += strlen(p) + 1; - } - } - while (HeapTupleIsValid(constraintTuple = systable_getnext(scan))) - { - Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(constraintTuple); ++ CatalogTupleUpdate(classRel, &otid, classTup); ++ UnlockTuple(classRel, &otid, InplaceUpdateTupleLock); - trigStmt = makeNode(CreateTrigStmt); - trigStmt->replace = false; - trigStmt->isconstraint = OidIsValid(trigForm->tgconstraint); - trigStmt->trigname = NameStr(trigForm->tgname); - trigStmt->relation = NULL; - trigStmt->funcname = NULL; /* passed separately */ - trigStmt->args = trigargs; - trigStmt->row = true; - trigStmt->timing = trigForm->tgtype & TRIGGER_TYPE_TIMING_MASK; - trigStmt->events = trigForm->tgtype & TRIGGER_TYPE_EVENT_MASK; - trigStmt->columns = cols; - trigStmt->whenClause = NULL; /* passed separately */ - trigStmt->transitionRels = NIL; /* not supported at present */ - trigStmt->deferrable = trigForm->tgdeferrable; - trigStmt->initdeferred = trigForm->tginitdeferred; - trigStmt->constrrel = NULL; /* passed separately */ - - CreateTriggerFiringOn(trigStmt, NULL, RelationGetRelid(partition), - trigForm->tgconstrrelid, InvalidOid, InvalidOid, - trigForm->tgfoid, trigForm->oid, qual, - false, true, trigForm->tgenabled); - - MemoryContextSwitchTo(oldcxt); - MemoryContextReset(perTupCxt); - 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); ++ /* 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); - MemoryContextDelete(perTupCxt); - nncolumns = lappend_int(nncolumns, attmap->attnums[parent_attno - 1]); - } ++ InvokeObjectPostAlterHook(RelationRelationId, relOid, 0); + } -- systable_endscan(scan); - table_close(pg_trigger, RowExclusiveLock); ++ heap_freetuple(classTup); +} - /* 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); +/* - * ALTER TABLE DETACH PARTITION - * - * Return the address of the relation that is no longer a partition of rel. - * - * If concurrent mode is requested, we run in two transactions. A side- - * effect is that this command cannot run in a multi-part ALTER TABLE. - * Currently, that's enforced by the grammar. ++ * Move all indexes for the specified relation to another namespace. + * - * The strategy for concurrency is to first modify the partition's - * pg_inherit catalog row to make it visible to everyone that the - * partition is detached, lock the partition against writes, and commit - * the transaction; anyone who requests the partition descriptor from - * that point onwards has to ignore such a partition. In a second - * transaction, we wait until all transactions that could have seen the - * partition as attached are gone, then we remove the rest of partition - * metadata (pg_inherits and pg_class.relpartbounds). ++ * Note: we assume adequate permission checking was done by the caller, ++ * and that the caller has a suitable lock on the owning relation. + */ - static ObjectAddress - ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel, - RangeVar *name, bool concurrent) ++static void ++AlterIndexNamespaces(Relation classRel, Relation rel, ++ Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved) +{ - Relation partRel; - ObjectAddress address; - Oid defaultPartOid; - - /* - * We must lock the default partition, because detaching this partition - * will change its partition constraint. - */ - defaultPartOid = - get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true)); - if (OidIsValid(defaultPartOid)) - { - /* - * Concurrent detaching when a default partition exists is not - * supported. The main problem is that the default partition - * constraint would change. And there's a definitional problem: what - * should happen to the tuples that are being inserted that belong to - * the partition being detached? Putting them on the partition being - * detached would be wrong, since they'd become "lost" after the - * detaching completes but we cannot put them in the default partition - * either until we alter its partition constraint. - * - * I think we could solve this problem if we effected the constraint - * change before committing the first transaction. But the lock would - * have to remain AEL and it would cause concurrent query planning to - * be blocked, so changing it that way would be even worse. - */ - if (concurrent) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("cannot detach partitions concurrently when a default partition exists"))); - LockRelationOid(defaultPartOid, AccessExclusiveLock); - } - - /* - * In concurrent mode, the partition is locked with share-update-exclusive - * in the first transaction. This allows concurrent transactions to be - * doing DML to the partition. - */ - partRel = table_openrv(name, concurrent ? ShareUpdateExclusiveLock : - AccessExclusiveLock); - - /* - * Check inheritance conditions and either delete the pg_inherits row (in - * non-concurrent mode) or just set the inhdetachpending flag. - */ - if (!concurrent) - RemoveInheritance(partRel, rel, false); - else - MarkInheritDetached(partRel, rel); ++ List *indexList; ++ ListCell *l; - /* - * Ensure that foreign keys still hold after this detach. This keeps - * locks on the referencing tables, which prevents concurrent transactions - * from adding rows that we wouldn't see. For this to work in concurrent - * mode, it is critical that the partition appears as no longer attached - * for the RI queries as soon as the first transaction commits. - */ - ATDetachCheckNoForeignKeyRefs(partRel); - while (HeapTupleIsValid(constraintTuple = systable_getnext(scan))) ++ indexList = RelationGetIndexList(rel); + - /* - * Concurrent mode has to work harder; first we add a new constraint to - * the partition that matches the partition constraint. Then we close our - * existing transaction, and in a new one wait for all processes to catch - * up on the catalog updates we've done so far; at that point we can - * complete the operation. - */ - if (concurrent) ++ foreach(l, indexList) { - Oid partrelid, - parentrelid; - LOCKTAG tag; - char *parentrelname; - char *partrelname; - - /* - * We're almost done now; the only traces that remain are the - * pg_inherits tuple and the partition's relpartbounds. Before we can - * remove those, we need to wait until all transactions that know that - * this is a partition are gone. - */ - - /* - * Remember relation OIDs to re-acquire them later; and relation names - * too, for error messages if something is dropped in between. - */ - partrelid = RelationGetRelid(partRel); - parentrelid = RelationGetRelid(rel); - parentrelname = MemoryContextStrdup(PortalContext, - RelationGetRelationName(rel)); - partrelname = MemoryContextStrdup(PortalContext, - RelationGetRelationName(partRel)); - - /* Invalidate relcache entries for the parent -- must be before close */ - CacheInvalidateRelcache(rel); - - table_close(partRel, NoLock); - table_close(rel, NoLock); - tab->rel = NULL; - - /* Make updated catalog entry visible */ - PopActiveSnapshot(); - CommitTransactionCommand(); - Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(constraintTuple); - bool match = false; ++ Oid indexOid = lfirst_oid(l); ++ ObjectAddress thisobj; + - StartTransactionCommand(); ++ thisobj.classId = RelationRelationId; ++ thisobj.objectId = indexOid; ++ thisobj.objectSubId = 0; /* - * Now wait. This ensures that all queries that were planned - * including the partition are finished before we remove the rest of - * catalog entries. We don't need or indeed want to acquire this - * lock, though -- that would block later queries. - * Match CHECK constraints by name, not-null constraints by column - * number, and ignore all others. ++ * 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. + * - * We don't need to concern ourselves with waiting for a lock on the - * partition itself, since we will acquire AccessExclusiveLock below. - */ - SET_LOCKTAG_RELATION(tag, MyDatabaseId, parentrelid); - WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, false); - - /* - * Now acquire locks in both relations again. Note they may have been - * removed in the meantime, so care is required. ++ * XXX this objsMoved test may be pointless -- surely we have a single ++ * dependency link from a relation to each index? */ - rel = try_relation_open(parentrelid, ShareUpdateExclusiveLock); - partRel = try_relation_open(partrelid, AccessExclusiveLock); - 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); -- - /* If the relations aren't there, something bad happened; bail out */ - if (rel == NULL) - foreach_int(prevattno, nncolumns) - { - if (prevattno == child_attno) - { - match = true; - nncolumns = foreach_delete_current(nncolumns, prevattno); - break; - } - } - } - else - continue; - - if (match) ++ if (!object_address_present(&thisobj, objsMoved)) { - if (partRel != NULL) /* shouldn't happen */ - elog(WARNING, "dangling partition \"%s\" remains, can't fix", - partrelname); - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("partitioned table \"%s\" was removed concurrently", - parentrelname))); - /* 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); ++ AlterRelationNamespaceInternal(classRel, indexOid, ++ oldNspOid, newNspOid, ++ false, objsMoved); ++ add_exact_object_address(&thisobj, objsMoved); } - if (partRel == NULL) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("partition \"%s\" was removed concurrently", partrelname))); - - tab->rel = rel; } - /* - * Detaching the partition might involve TOAST table access, so ensure we - * have a valid snapshot. - */ - PushActiveSnapshot(GetTransactionSnapshot()); - - /* Do the final part of detaching */ - DetachPartitionFinalize(rel, partRel, concurrent, defaultPartOid); - - PopActiveSnapshot(); - /* 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)); -- - ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel)); - systable_endscan(scan); - table_close(catalogRelation, RowExclusiveLock); -- - /* keep our lock until commit */ - table_close(partRel, NoLock); - drop_parent_dependency(RelationGetRelid(child_rel), - RelationRelationId, - RelationGetRelid(parent_rel), - child_dependency_type(is_partitioning)); -- - return address; - /* - * 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); ++ list_free(indexList); } /* - * Second part of ALTER TABLE .. DETACH. - * 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. ++ * Move all identity and SERIAL-column sequences of the specified relation to another ++ * namespace. + * - * This is separate so that it can be run independently when the second - * transaction of the concurrent algorithm fails (crash or abort). ++ * 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 - DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent, - Oid defaultPartOid) -drop_parent_dependency(Oid relid, Oid refclassid, Oid refobjid, - DependencyType deptype) ++AlterSeqNamespaces(Relation classRel, Relation rel, ++ Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved, ++ LOCKMODE lockmode) { - Relation classRel; - List *fks; - ListCell *cell; - List *indexes; - Datum new_val[Natts_pg_class]; - bool new_null[Natts_pg_class], - new_repl[Natts_pg_class]; - HeapTuple tuple, - newtuple; - Relation trigrel = NULL; - List *fkoids = NIL; - - if (concurrent) - { - /* - * We can remove the pg_inherits row now. (In the non-concurrent case, - * this was already done). - */ - RemoveInheritance(partRel, rel, true); - } - - /* Drop any triggers that were cloned on creation/attach. */ - DropClonedTriggersFromPartition(RelationGetRelid(partRel)); - Relation catalogRelation; ++ Relation depRel; + SysScanDesc scan; - ScanKeyData key[3]; - HeapTuple depTuple; ++ ScanKeyData key[2]; ++ HeapTuple tup; - catalogRelation = table_open(DependRelationId, RowExclusiveLock); + /* - * Detach any foreign keys that are inherited. This includes creating - * additional action triggers. ++ * SERIAL sequences are those having an auto dependency on one of the ++ * table's columns (we don't care *which* column, exactly). + */ - fks = copyObject(RelationGetFKeyList(partRel)); - if (fks != NIL) - trigrel = table_open(TriggerRelationId, RowExclusiveLock); ++ depRel = table_open(DependRelationId, AccessShareLock); - /* - * It's possible that the partition being detached has a foreign key that - * references a partitioned table. When that happens, there are multiple - * pg_constraint rows for the partition: one points to the partitioned - * table itself, while the others point to each of its partitions. Only - * the topmost one is to be considered here; the child constraints must be - * left alone, because conceptually those aren't coming from our parent - * partitioned table, but from this partition itself. - * - * We implement this by collecting all the constraint OIDs in a first scan - * of the FK array, and skipping in the loop below those constraints whose - * parents are listed here. - */ - foreach_node(ForeignKeyCacheInfo, fk, fks) - fkoids = lappend_oid(fkoids, fk->conoid); + ScanKeyInit(&key[0], - Anum_pg_depend_classid, ++ Anum_pg_depend_refclassid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationRelationId)); + ScanKeyInit(&key[1], - Anum_pg_depend_objid, ++ Anum_pg_depend_refobjid, + BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(relid)); - ScanKeyInit(&key[2], - Anum_pg_depend_objsubid, - BTEqualStrategyNumber, F_INT4EQ, - Int32GetDatum(0)); ++ ObjectIdGetDatum(RelationGetRelid(rel))); ++ /* we leave refobjsubid unspecified */ - foreach(cell, fks) - { - ForeignKeyCacheInfo *fk = lfirst(cell); - HeapTuple contup; - Form_pg_constraint conform; - scan = systable_beginscan(catalogRelation, DependDependerIndexId, true, - NULL, 3, key); ++ scan = systable_beginscan(depRel, DependReferenceIndexId, true, ++ NULL, 2, key); - contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid)); - if (!HeapTupleIsValid(contup)) - elog(ERROR, "cache lookup failed for constraint %u", fk->conoid); - conform = (Form_pg_constraint) GETSTRUCT(contup); - while (HeapTupleIsValid(depTuple = systable_getnext(scan))) ++ while (HeapTupleIsValid(tup = systable_getnext(scan))) + { - Form_pg_depend dep = (Form_pg_depend) GETSTRUCT(depTuple); ++ Form_pg_depend depForm = (Form_pg_depend) GETSTRUCT(tup); ++ Relation seqRel; - /* - * Consider only inherited foreign keys, and only if their parents - * aren't in the list. - */ - if (conform->contype != CONSTRAINT_FOREIGN || - !OidIsValid(conform->conparentid) || - list_member_oid(fkoids, conform->conparentid)) - { - ReleaseSysCache(contup); - if (dep->refclassid == refclassid && - dep->refobjid == refobjid && - dep->refobjsubid == 0 && - dep->deptype == deptype) - CatalogTupleDelete(catalogRelation, &depTuple->t_self); ++ /* 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; - } + - /* - * The constraint on this table must be marked no longer a child of - * the parent's constraint, as do its check triggers. - */ - ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid); ++ /* Use relation_open just in case it's an index */ ++ seqRel = relation_open(depForm->objid, lockmode); + - /* - * Also, look up the partition's "check" triggers corresponding to the - * ENFORCED constraint being detached and detach them from the parent - * triggers. NOT ENFORCED constraints do not have these triggers; - * therefore, this step is not needed. - */ - if (fk->conenforced) ++ /* skip non-sequence relations */ ++ if (RelationGetForm(seqRel)->relkind != RELKIND_SEQUENCE) + { - Oid insertTriggerOid, - updateTriggerOid; - - GetForeignKeyCheckTriggers(trigrel, - fk->conoid, fk->confrelid, fk->conrelid, - &insertTriggerOid, &updateTriggerOid); - Assert(OidIsValid(insertTriggerOid)); - TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid, - RelationGetRelid(partRel)); - Assert(OidIsValid(updateTriggerOid)); - TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid, - RelationGetRelid(partRel)); ++ /* 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); ++ + /* - * Lastly, create the action triggers on the referenced table, using - * addFkRecurseReferenced, which requires some elaborate setup (so put - * it in a separate block). While at it, if the table is partitioned, - * that function will recurse to create the pg_constraint rows and - * action triggers for each partition. - * - * Note there's no need to do addFkConstraint() here, because the - * pg_constraint row already exists. ++ * 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). + */ - { - Constraint *fkconstraint; - int numfks; - AttrNumber conkey[INDEX_MAX_KEYS]; - AttrNumber confkey[INDEX_MAX_KEYS]; - Oid conpfeqop[INDEX_MAX_KEYS]; - Oid conppeqop[INDEX_MAX_KEYS]; - Oid conffeqop[INDEX_MAX_KEYS]; - int numfkdelsetcols; - AttrNumber confdelsetcols[INDEX_MAX_KEYS]; - Relation refdRel; - - DeconstructFkConstraintRow(contup, - &numfks, - conkey, - confkey, - conpfeqop, - conppeqop, - conffeqop, - &numfkdelsetcols, - confdelsetcols); - - /* Create a synthetic node we'll use throughout */ - fkconstraint = makeNode(Constraint); - fkconstraint->contype = CONSTRAINT_FOREIGN; - fkconstraint->conname = pstrdup(NameStr(conform->conname)); - fkconstraint->deferrable = conform->condeferrable; - fkconstraint->initdeferred = conform->condeferred; - fkconstraint->is_enforced = conform->conenforced; - fkconstraint->skip_validation = true; - fkconstraint->initially_valid = conform->convalidated; - /* a few irrelevant fields omitted here */ - fkconstraint->pktable = NULL; - fkconstraint->fk_attrs = NIL; - fkconstraint->pk_attrs = NIL; - fkconstraint->fk_matchtype = conform->confmatchtype; - fkconstraint->fk_upd_action = conform->confupdtype; - fkconstraint->fk_del_action = conform->confdeltype; - fkconstraint->fk_del_set_cols = NIL; - fkconstraint->old_conpfeqop = NIL; - fkconstraint->old_pktable_oid = InvalidOid; - fkconstraint->location = -1; - - /* set up colnames, used to generate the constraint name */ - for (int i = 0; i < numfks; i++) - { - Form_pg_attribute att; - - att = TupleDescAttr(RelationGetDescr(partRel), - conkey[i] - 1); - - fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs, - makeString(NameStr(att->attname))); - } - - refdRel = table_open(fk->confrelid, ShareRowExclusiveLock); - - addFkRecurseReferenced(fkconstraint, partRel, - refdRel, - conform->conindid, - fk->conoid, - numfks, - confkey, - conkey, - conpfeqop, - conppeqop, - conffeqop, - numfkdelsetcols, - confdelsetcols, - true, - InvalidOid, InvalidOid, - conform->conperiod); - table_close(refdRel, NoLock); /* keep lock till end of xact */ - } - - ReleaseSysCache(contup); - } - list_free_deep(fks); - if (trigrel) - table_close(trigrel, RowExclusiveLock); - - /* - * Any sub-constraints that are in the referenced-side of a larger - * constraint have to be removed. This partition is no longer part of the - * key space of the constraint. - */ - foreach(cell, GetParentedForeignKeyRefs(partRel)) - { - Oid constrOid = lfirst_oid(cell); - ObjectAddress constraint; - - ConstraintSetParentConstraint(constrOid, InvalidOid, InvalidOid); - deleteDependencyRecordsForClass(ConstraintRelationId, - constrOid, - ConstraintRelationId, - DEPENDENCY_INTERNAL); - CommandCounterIncrement(); ++ Assert(RelationGetForm(seqRel)->reltype == InvalidOid); + - ObjectAddressSet(constraint, ConstraintRelationId, constrOid); - performDeletion(&constraint, DROP_RESTRICT, 0); ++ /* Now we can close it. Keep the lock till end of transaction. */ ++ relation_close(seqRel, NoLock); } - /* Now we can detach indexes */ - indexes = RelationGetIndexList(partRel); - foreach(cell, indexes) - { - Oid idxid = lfirst_oid(cell); - Oid parentidx; - Relation idx; - Oid constrOid; - Oid parentConstrOid; - - if (!has_superclass(idxid)) - continue; - - parentidx = get_partition_parent(idxid, false); - Assert((IndexGetRelation(parentidx, false) == RelationGetRelid(rel))); + systable_endscan(scan); - table_close(catalogRelation, RowExclusiveLock); + - idx = index_open(idxid, AccessExclusiveLock); - IndexSetParentIndex(idx, InvalidOid); ++ relation_close(depRel, AccessShareLock); + } - /* - * If there's a constraint associated with the index, detach it too. - * Careful: it is possible for a constraint index in a partition to be - * the child of a non-constraint index, so verify whether the parent - * index does actually have a constraint. - */ - constrOid = get_relation_idx_constraint_oid(RelationGetRelid(partRel), - idxid); - parentConstrOid = get_relation_idx_constraint_oid(RelationGetRelid(rel), - parentidx); - if (OidIsValid(parentConstrOid) && OidIsValid(constrOid)) - ConstraintSetParentConstraint(constrOid, InvalidOid, InvalidOid); + - index_close(idx, NoLock); - } + /* - * 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. ++ * This code supports ++ * CREATE TEMP TABLE ... ON COMMIT { DROP | PRESERVE ROWS | DELETE ROWS } + * - * The address of the type is returned. ++ * Because we only support this for TEMP tables, it's sufficient to remember ++ * the state in a backend-local data structure. + */ -static ObjectAddress -ATExecAddOf(Relation rel, const TypeName *ofTypename, LOCKMODE lockmode) + - /* Update pg_class tuple */ - classRel = table_open(RelationRelationId, RowExclusiveLock); - tuple = SearchSysCacheCopy1(RELOID, - ObjectIdGetDatum(RelationGetRelid(partRel))); - if (!HeapTupleIsValid(tuple)) - elog(ERROR, "cache lookup failed for relation %u", - RelationGetRelid(partRel)); - Assert(((Form_pg_class) GETSTRUCT(tuple))->relispartition); - - /* Clear relpartbound and reset relispartition */ - memset(new_val, 0, sizeof(new_val)); - memset(new_null, false, sizeof(new_null)); - memset(new_repl, false, sizeof(new_repl)); - new_val[Anum_pg_class_relpartbound - 1] = (Datum) 0; - new_null[Anum_pg_class_relpartbound - 1] = true; - new_repl[Anum_pg_class_relpartbound - 1] = true; - newtuple = heap_modify_tuple(tuple, RelationGetDescr(classRel), - new_val, new_null, new_repl); - - ((Form_pg_class) GETSTRUCT(newtuple))->relispartition = false; - CatalogTupleUpdate(classRel, &newtuple->t_self, newtuple); - heap_freetuple(newtuple); - table_close(classRel, RowExclusiveLock); ++/* ++ * Register a newly-created relation's ON COMMIT action. ++ */ ++void ++register_on_commit_action(Oid relid, OnCommitAction action) + { - 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; ++ OnCommitItem *oc; ++ MemoryContext oldcxt; - /* Validate the type. */ - typetuple = typenameType(NULL, ofTypename, NULL); - check_of_type(typetuple); - typeform = (Form_pg_type) GETSTRUCT(typetuple); - typeid = typeform->oid; + /* - * Drop identity property from all identity columns of partition. ++ * We needn't bother registering the relation unless there is an ON COMMIT ++ * action we need to take. + */ - for (int attno = 0; attno < RelationGetNumberOfAttributes(partRel); attno++) - { - Form_pg_attribute attr = TupleDescAttr(partRel->rd_att, attno); ++ if (action == ONCOMMIT_NOOP || action == ONCOMMIT_PRESERVE_ROWS) ++ return; - if (!attr->attisdropped && attr->attidentity) - ATExecDropIdentity(partRel, NameStr(attr->attname), false, - AccessExclusiveLock, true, true); - } - /* 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); ++ oldcxt = MemoryContextSwitchTo(CacheMemoryContext); + - if (OidIsValid(defaultPartOid)) - { - /* - * If the relation being detached is the default partition itself, - * remove it from the parent's pg_partitioned_table entry. - * - * If not, we must invalidate default partition's relcache entry, as - * in StorePartitionBound: its partition constraint depends on every - * other partition's partition constraint. - */ - if (RelationGetRelid(partRel) == defaultPartOid) - update_default_partition_oid(RelationGetRelid(rel), InvalidOid); - else - CacheInvalidateRelcacheByRelid(defaultPartOid); - } ++ oc = (OnCommitItem *) palloc(sizeof(OnCommitItem)); ++ oc->relid = relid; ++ oc->oncommit = action; ++ oc->creating_subid = GetCurrentSubTransactionId(); ++ oc->deleting_subid = InvalidSubTransactionId; /* - * Invalidate the parent's relcache so that the partition is no longer - * included in its partition descriptor. - * Check the tuple descriptors for compatibility. Unlike inheritance, we - * require that the order also match. However, attnotnull need not match. ++ * 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. */ - CacheInvalidateRelcache(rel); - 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; ++ on_commits = lcons(oc, on_commits); - /* - * If the partition we just detached is partitioned itself, invalidate - * relcache for all descendent partitions too to ensure that their - * rd_partcheck expression trees are rebuilt; must lock partitions before - * doing so, using the same lockmode as what partRel has been locked with - * by the caller. - */ - if (partRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) - /* 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); ++ MemoryContextSwitchTo(oldcxt); ++} + - /* 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); ++/* ++ * 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; + - /* 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))); ++ foreach(l, on_commits) + { - List *children; ++ OnCommitItem *oc = (OnCommitItem *) lfirst(l); - children = find_all_inheritors(RelationGetRelid(partRel), - AccessExclusiveLock, NULL); - foreach(cell, children) - /* 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))); ++ if (oc->relid == relid) + { - CacheInvalidateRelcacheByRelid(lfirst_oid(cell)); ++ oc->deleting_subid = GetCurrentSubTransactionId(); ++ break; + } } - ReleaseTupleDesc(typeTupleDesc); +} - /* Any remaining columns at the end of the table had better be dropped. */ - for (; table_attno <= tableTupleDesc->natts; table_attno++) +/* - * ALTER TABLE ... DETACH PARTITION ... FINALIZE ++ * Perform ON COMMIT actions. + * - * To use when a DETACH PARTITION command previously did not run to - * completion; this completes the detaching process. ++ * This is invoked just before actually committing, since it's possible ++ * to encounter errors. + */ - static ObjectAddress - ATExecDetachPartitionFinalize(Relation rel, RangeVar *name) ++void ++PreCommit_on_commit_actions(void) +{ - Relation partRel; - ObjectAddress address; - Snapshot snap = GetActiveSnapshot(); ++ ListCell *l; ++ List *oids_to_truncate = NIL; ++ List *oids_to_drop = NIL; + - partRel = table_openrv(name, AccessExclusiveLock); ++ foreach(l, on_commits) + { - Form_pg_attribute table_attr = TupleDescAttr(tableTupleDesc, - table_attno - 1); ++ OnCommitItem *oc = (OnCommitItem *) lfirst(l); - /* - * Wait until existing snapshots are gone. This is important if the - * second transaction of DETACH PARTITION CONCURRENTLY is canceled: the - * user could immediately run DETACH FINALIZE without actually waiting for - * existing transactions. We must not complete the detach action until - * all such queries are complete (otherwise we would present them with an - * inconsistent view of catalogs). - */ - WaitForOlderSnapshots(snap->xmin, false); - if (!table_attr->attisdropped) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("table has extra column \"%s\"", - NameStr(table_attr->attname)))); - } ++ /* Ignore entry if already dropped in this xact */ ++ if (oc->deleting_subid != InvalidSubTransactionId) ++ continue; - DetachPartitionFinalize(rel, partRel, true, InvalidOid); - /* 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); ++ switch (oc->oncommit) ++ { ++ case ONCOMMIT_NOOP: ++ case ONCOMMIT_PRESERVE_ROWS: ++ /* Do nothing (there shouldn't be such entries, actually) */ ++ break; ++ case ONCOMMIT_DELETE_ROWS: - ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel)); - /* 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); ++ /* ++ * 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; ++ } ++ } - table_close(partRel, NoLock); - /* 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); ++ /* ++ * 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); - return address; - } - InvokeObjectPostAlterHook(RelationRelationId, relid, 0); ++ if (oids_to_drop != NIL) ++ { ++ ObjectAddresses *targetObjects = new_object_addresses(); - /* - * DropClonedTriggersFromPartition - * subroutine for ATExecDetachPartition to remove any triggers that were - * cloned to the partition when it was created-as-partition or attached. - * This undoes what CloneRowTriggersToPartition did. - */ - static void - DropClonedTriggersFromPartition(Oid partitionId) - { - ScanKeyData skey; - SysScanDesc scan; - HeapTuple trigtup; - Relation tgrel; - ObjectAddresses *objects; - heap_freetuple(classtuple); - table_close(relationRelation, RowExclusiveLock); ++ foreach(l, oids_to_drop) ++ { ++ ObjectAddress object; - objects = new_object_addresses(); - ReleaseSysCache(typetuple); ++ object.classId = RelationRelationId; ++ object.objectId = lfirst_oid(l); ++ object.objectSubId = 0; - /* - * Scan pg_trigger to search for all triggers on this rel. - */ - ScanKeyInit(&skey, Anum_pg_trigger_tgrelid, BTEqualStrategyNumber, - F_OIDEQ, ObjectIdGetDatum(partitionId)); - tgrel = table_open(TriggerRelationId, RowExclusiveLock); - scan = systable_beginscan(tgrel, TriggerRelidNameIndexId, - true, NULL, 1, &skey); - while (HeapTupleIsValid(trigtup = systable_getnext(scan))) - { - Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(trigtup); - ObjectAddress trig; - return typeobj; -} ++ Assert(!object_address_present(&object, targetObjects)); - /* Ignore triggers that weren't cloned */ - if (!OidIsValid(pg_trigger->tgparentid)) - continue; -/* - * 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; ++ add_exact_object_address(&object, targetObjects); ++ } - if (!OidIsValid(rel->rd_rel->reloftype)) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is not a typed table", - RelationGetRelationName(rel)))); + /* - * Ignore internal triggers that are implementation objects of foreign - * keys, because these will be detached when the foreign keys - * themselves are. ++ * Object deletion might involve toast table access (to clean up ++ * toasted catalog entries), so ensure we have a valid snapshot. + */ - if (OidIsValid(pg_trigger->tgconstrrelid)) - continue; ++ PushActiveSnapshot(GetTransactionSnapshot()); - /* - * 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. - */ + /* - * This is ugly, but necessary: remove the dependency markings on the - * trigger so that it can be removed. ++ * Since this is an automatic drop, rather than one directly initiated ++ * by the user, we pass the PERFORM_DELETION_INTERNAL flag. + */ - deleteDependencyRecordsForClass(TriggerRelationId, pg_trigger->oid, - TriggerRelationId, - DEPENDENCY_PARTITION_PRI); - deleteDependencyRecordsForClass(TriggerRelationId, pg_trigger->oid, - RelationRelationId, - DEPENDENCY_PARTITION_SEC); ++ performMultipleDeletions(targetObjects, DROP_CASCADE, ++ PERFORM_DELETION_INTERNAL | PERFORM_DELETION_QUIETLY); - /* remember this trigger to remove it below */ - ObjectAddressSet(trig, TriggerRelationId, pg_trigger->oid); - add_exact_object_address(&trig, objects); - } - drop_parent_dependency(relid, TypeRelationId, rel->rd_rel->reloftype, - DEPENDENCY_NORMAL); ++ PopActiveSnapshot(); - /* make the dependency removal visible to the deletion below */ - CommandCounterIncrement(); - performMultipleDeletions(objects, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); - /* 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); ++#ifdef USE_ASSERT_CHECKING - /* done */ - free_object_addresses(objects); - systable_endscan(scan); - table_close(tgrel, RowExclusiveLock); - InvokeObjectPostAlterHook(RelationRelationId, relid, 0); ++ /* ++ * 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); + - heap_freetuple(tuple); - table_close(relationRelation, RowExclusiveLock); ++ if (oc->oncommit != ONCOMMIT_DROP) ++ continue; ++ ++ Assert(oc->deleting_subid != InvalidSubTransactionId); ++ } ++#endif ++ } } /* - * Before acquiring lock on an index, acquire the same lock on the owning - * table. - * relation_mark_replica_identity: Update a table's replica identity ++ * Post-commit or post-abort cleanup for ON COMMIT management. + * - * Iff ri_type = REPLICA_IDENTITY_INDEX, indexOid must be the Oid of a suitable - * index. Otherwise, it must be InvalidOid. ++ * All we do here is remove no-longer-needed OnCommitItem entries. + * - * Caller had better hold an exclusive lock on the relation, as the results - * of running two of these concurrently wouldn't be pretty. ++ * During commit, remove entries that were deleted during this transaction; ++ * during abort, remove those created during this transaction. */ - struct AttachIndexCallbackState - { - Oid partitionOid; - Oid parentTblOid; - bool lockedParentTbl; - }; - --static void - RangeVarCallbackForAttachIndex(const RangeVar *rv, Oid relOid, Oid oldRelOid, - void *arg) -relation_mark_replica_identity(Relation rel, char ri_type, Oid indexOid, - bool is_internal) ++void ++AtEOXact_on_commit_actions(bool isCommit) { - struct AttachIndexCallbackState *state; - Form_pg_class classform; - HeapTuple tuple; - 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; -- - state = (struct AttachIndexCallbackState *) arg; - /* - * 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); ++ ListCell *cur_item; - if (!state->lockedParentTbl) - /* - * Update the per-index indisreplident flags correctly. - */ - pg_index = table_open(IndexRelationId, RowExclusiveLock); - foreach(index, RelationGetIndexList(rel)) ++ foreach(cur_item, on_commits) { - LockRelationOid(state->parentTblOid, AccessShareLock); - state->lockedParentTbl = true; - } - 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); ++ OnCommitItem *oc = (OnCommitItem *) lfirst(cur_item); - /* - * 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; - if (thisIndexOid == indexOid) ++ if (isCommit ? oc->deleting_subid != InvalidSubTransactionId : ++ oc->creating_subid != InvalidSubTransactionId) + { - /* Set the bit if not already set. */ - if (!pg_index_form->indisreplident) - { - dirty = true; - pg_index_form->indisreplident = true; - } ++ /* cur_item must be removed */ ++ on_commits = foreach_delete_current(on_commits, cur_item); ++ pfree(oc); + } + 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); ++ /* cur_item must be preserved */ ++ oc->creating_subid = InvalidSubTransactionId; ++ oc->deleting_subid = InvalidSubTransactionId; + } - heap_freetuple(pg_index_tuple); } - - /* Didn't find a relation, so no need for locking or permission checks. */ - if (!OidIsValid(relOid)) - return; - - 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) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("\"%s\" is not an index", rv->relname))); - ReleaseSysCache(tuple); -- - /* - * Since we need only examine the heap's tupledesc, an access share lock - * on it (preventing any DDL) is sufficient. - */ - state->partitionOid = IndexGetRelation(relOid, false); - LockRelationOid(state->partitionOid, AccessShareLock); - table_close(pg_index, RowExclusiveLock); } /* - * ALTER INDEX i1 ATTACH PARTITION i2 - * ALTER TABLE REPLICA IDENTITY ... ++ * 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. */ - static ObjectAddress - ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, RangeVar *name) -static void -ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode) ++void ++AtEOSubXact_on_commit_actions(bool isCommit, SubTransactionId mySubid, ++ SubTransactionId parentSubid) { - 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)); - Oid indexOid; - Relation indexRel; - int key; ++ ListCell *cur_item; - /* Silently do nothing if already in the right state */ - currParent = partIdx->rd_rel->relispartition ? - get_partition_parent(partIdxId, false) : InvalidOid; - if (currParent != RelationGetRelid(parentIdx)) - 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) ++ foreach(cur_item, on_commits) { - IndexInfo *childInfo; - IndexInfo *parentInfo; - AttrMap *attmap; - bool found; - int i; - PartitionDesc partDesc; - Oid constraintOid, - cldConstrId = InvalidOid; - - /* - * If this partition already has an index attached, refuse the - * operation. - */ - refuseDupeIndexAttach(parentIdx, partIdx, partTbl); - /* fallthrough */ ; - } - else - elog(ERROR, "unexpected identity type %u", stmt->identity_type); ++ OnCommitItem *oc = (OnCommitItem *) lfirst(cur_item); - 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++) - /* 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)))); ++ if (!isCommit && oc->creating_subid == mySubid) + { - if (partDesc->oids[i] == state.partitionOid) - { - found = true; - break; - } ++ /* cur_item must be removed */ ++ on_commits = foreach_delete_current(on_commits, cur_item); ++ pfree(oc); + } - 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)) - 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."))); - - /* - * 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)); - - if (OidIsValid(constraintOid)) ++ else + { - 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)))); ++ /* 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; + } - - /* - * If it's a primary key, make sure the columns in the partition are - * NOT NULL. - */ - if (parentIdx->rd_index->indisprimary) - verifyPartitionIndexNotNull(childInfo, partTbl); - - /* All good -- do it */ - IndexSetParentIndex(partIdx, RelationGetRelid(parentIdx)); - if (OidIsValid(constraintOid)) - ConstraintSetParentConstraint(cldConstrId, constraintOid, - RelationGetRelid(partTbl)); - - free_attrmap(attmap); - - validatePartitionedIndex(parentIdx, parentTbl); + } - - relation_close(parentTbl, AccessShareLock); - /* keep these locks till commit */ - relation_close(partTbl, NoLock); - relation_close(partIdx, NoLock); - - return address; +} - indexRel = index_open(indexOid, ShareLock); +/* - * Verify whether the given partition already contains an index attached - * to the given partitioned index. If so, raise an error. ++ * 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. + */ - static void - refuseDupeIndexAttach(Relation parentIdx, Relation partIdx, Relation partitionTbl) ++void ++RangeVarCallbackMaintainsTable(const RangeVar *relation, ++ Oid relId, Oid oldRelId, void *arg) +{ - Oid existingIdx; ++ char relkind; ++ AclResult aclresult; + - /* 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)))); ++ /* Nothing to do if the relation was not found. */ ++ if (!OidIsValid(relId)) ++ return; - existingIdx = index_get_partition(partitionTbl, - RelationGetRelid(parentIdx)); - if (OidIsValid(existingIdx)) + /* - * 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 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. + */ - if ((!indexRel->rd_indam->amcanunique || - !indexRel->rd_index->indisunique) && - !(indexRel->rd_index->indisunique && indexRel->rd_index->indisexclusion)) ++ 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_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)))); + (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); ++ errmsg("\"%s\" is not a table or materialized view", relation->relname))); + - index_close(indexRel, NoLock); ++ /* 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); } /* - * Verify whether the set of attached partition indexes to a parent index on - * a partitioned table is complete. If it is, mark the parent index valid. - * - * This should be called each time a partition index is attached. - * ALTER TABLE ENABLE/DISABLE ROW LEVEL SECURITY ++ * Callback to RangeVarGetRelidExtended() for TRUNCATE processing. */ static void - validatePartitionedIndex(Relation partedIdx, Relation partedTbl) -ATExecSetRowSecurity(Relation rel, bool rls) ++RangeVarCallbackForTruncate(const RangeVar *relation, ++ Oid relId, Oid oldRelId, void *arg) { - Relation inheritsRel; - SysScanDesc scan; - ScanKeyData key; - int tuples = 0; - HeapTuple inhTup; - bool updated = false; - - Assert(partedIdx->rd_rel->relkind == RELKIND_PARTITIONED_INDEX); - - /* - * Scan pg_inherits for this parent index. Count each valid index we find - * (verifying the pg_index entry for each), and if we reach the total - * amount we expect, we can mark this parent index as valid. - */ - inheritsRel = table_open(InheritsRelationId, AccessShareLock); - ScanKeyInit(&key, Anum_pg_inherits_inhparent, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(RelationGetRelid(partedIdx))); - scan = systable_beginscan(inheritsRel, InheritsParentIndexId, true, - NULL, 1, &key); - while ((inhTup = systable_getnext(scan)) != NULL) - { - Form_pg_inherits inhForm = (Form_pg_inherits) GETSTRUCT(inhTup); - HeapTuple indTup; - Form_pg_index indexForm; - - indTup = SearchSysCache1(INDEXRELID, - ObjectIdGetDatum(inhForm->inhrelid)); - if (!HeapTupleIsValid(indTup)) - elog(ERROR, "cache lookup failed for index %u", inhForm->inhrelid); - indexForm = (Form_pg_index) GETSTRUCT(indTup); - if (indexForm->indisvalid) - tuples += 1; - ReleaseSysCache(indTup); - } - - /* Done with pg_inherits */ - systable_endscan(scan); - table_close(inheritsRel, AccessShareLock); - Relation pg_class; - Oid relid; + HeapTuple tuple; - /* - * If we found as many inherited indexes as the partitioned table has - * partitions, we're good; update pg_index to set indisvalid. - */ - if (tuples == RelationGetPartitionDesc(partedTbl, true)->nparts) - { - Relation idxRel; - HeapTuple indTup; - Form_pg_index indexForm; - 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); ++ /* Nothing to do if the relation was not found. */ ++ if (!OidIsValid(relId)) ++ return; - idxRel = table_open(IndexRelationId, RowExclusiveLock); - indTup = SearchSysCacheCopy1(INDEXRELID, - ObjectIdGetDatum(RelationGetRelid(partedIdx))); - if (!HeapTupleIsValid(indTup)) - elog(ERROR, "cache lookup failed for index %u", - RelationGetRelid(partedIdx)); - indexForm = (Form_pg_index) GETSTRUCT(indTup); - ((Form_pg_class) GETSTRUCT(tuple))->relrowsecurity = rls; - CatalogTupleUpdate(pg_class, &tuple->t_self, tuple); ++ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relId)); ++ if (!HeapTupleIsValid(tuple)) /* should not happen */ ++ elog(ERROR, "cache lookup failed for relation %u", relId); - indexForm->indisvalid = true; - updated = true; - InvokeObjectPostAlterHook(RelationRelationId, - RelationGetRelid(rel), 0); ++ truncate_check_rel(relId, (Form_pg_class) GETSTRUCT(tuple)); ++ truncate_check_perms(relId, (Form_pg_class) GETSTRUCT(tuple)); - CatalogTupleUpdate(idxRel, &indTup->t_self, indTup); - table_close(pg_class, RowExclusiveLock); - heap_freetuple(tuple); ++ ReleaseSysCache(tuple); + } - table_close(idxRel, RowExclusiveLock); - heap_freetuple(indTup); - } + /* - * ALTER TABLE FORCE/NO FORCE ROW LEVEL SECURITY ++ * Callback for RangeVarGetRelidExtended(). Checks that the current user is ++ * the owner of the relation, or superuser. + */ -static void -ATExecForceNoForceRowSecurity(Relation rel, bool force_rls) ++void ++RangeVarCallbackOwnsRelation(const RangeVar *relation, ++ Oid relId, Oid oldRelId, void *arg) + { - Relation pg_class; - Oid relid; + HeapTuple tuple; - /* - * If this index is in turn a partition of a larger index, validating it - * might cause the parent to become valid also. Try that. - */ - if (updated && partedIdx->rd_rel->relispartition) - { - Oid parentIdxId, - parentTblId; - Relation parentIdx, - parentTbl; - relid = RelationGetRelid(rel); - - pg_class = table_open(RelationRelationId, RowExclusiveLock); - - tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid)); ++ /* Nothing to do if the relation was not found. */ ++ if (!OidIsValid(relId)) ++ return; - /* make sure we see the validation we just did */ - CommandCounterIncrement(); - if (!HeapTupleIsValid(tuple)) - elog(ERROR, "cache lookup failed for relation %u", relid); ++ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relId)); ++ if (!HeapTupleIsValid(tuple)) /* should not happen */ ++ elog(ERROR, "cache lookup failed for relation %u", relId); - parentIdxId = get_partition_parent(RelationGetRelid(partedIdx), false); - parentTblId = get_partition_parent(RelationGetRelid(partedTbl), false); - parentIdx = relation_open(parentIdxId, AccessExclusiveLock); - parentTbl = relation_open(parentTblId, AccessExclusiveLock); - Assert(!parentIdx->rd_index->indisvalid); - ((Form_pg_class) GETSTRUCT(tuple))->relforcerowsecurity = force_rls; - CatalogTupleUpdate(pg_class, &tuple->t_self, tuple); ++ if (!object_ownercheck(RelationRelationId, relId, GetUserId())) ++ aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relId)), ++ relation->relname); - validatePartitionedIndex(parentIdx, parentTbl); - InvokeObjectPostAlterHook(RelationRelationId, - RelationGetRelid(rel), 0); ++ 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))); - relation_close(parentIdx, AccessExclusiveLock); - relation_close(parentTbl, AccessExclusiveLock); - } - table_close(pg_class, RowExclusiveLock); - heap_freetuple(tuple); ++ ReleaseSysCache(tuple); } /* - * When attaching an index as a partition of a partitioned index which is a - * primary key, verify that all the columns in the partition are marked NOT - * NULL. - * ALTER FOREIGN TABLE OPTIONS (...) ++ * Common RangeVarGetRelid callback for rename, set schema, and alter table ++ * processing. */ static void - verifyPartitionIndexNotNull(IndexInfo *iinfo, Relation partition) -ATExecGenericOptions(Relation rel, List *options) ++RangeVarCallbackForAlterRelation(const RangeVar *rv, Oid relid, Oid oldrelid, ++ void *arg) { - for (int i = 0; i < iinfo->ii_NumIndexKeyAttrs; i++) - { - Form_pg_attribute att = TupleDescAttr(RelationGetDescr(partition), - iinfo->ii_IndexAttrNumbers[i] - 1); - Relation ftrel; - ForeignServer *server; - ForeignDataWrapper *fdw; ++ Node *stmt = (Node *) arg; ++ ObjectType reltype; + 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; ++ Form_pg_class classform; ++ AclResult aclresult; ++ char relkind; - if (!att->attnotnull) - ereport(ERROR, - errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("invalid primary key definition"), - errdetail("Column \"%s\" of relation \"%s\" is not marked NOT NULL.", - NameStr(att->attname), - RelationGetRelationName(partition))); - } - } - if (options == NIL) - return; ++ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid)); ++ if (!HeapTupleIsValid(tuple)) ++ return; /* concurrently dropped */ ++ classform = (Form_pg_class) GETSTRUCT(tuple); ++ relkind = classform->relkind; - /* - * Return an OID list of constraints that reference the given relation - * that are marked as having a parent constraints. - */ - static List * - GetParentedForeignKeyRefs(Relation partition) - { - Relation pg_constraint; - HeapTuple tuple; - SysScanDesc scan; - ScanKeyData key[2]; - List *constraints = NIL; - ftrel = table_open(ForeignTableRelationId, RowExclusiveLock); ++ /* Must own relation. */ ++ if (!object_ownercheck(RelationRelationId, relid, GetUserId())) ++ aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relid)), rv->relname); + - tuple = SearchSysCacheCopy1(FOREIGNTABLEREL, - ObjectIdGetDatum(rel->rd_id)); - if (!HeapTupleIsValid(tuple)) ++ /* No system table modifications unless explicitly allowed. */ ++ if (!allowSystemTableMods && IsSystemClass(relid, classform)) + 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); ++ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), ++ errmsg("permission denied: \"%s\" is a system catalog", ++ rv->relname))); - memset(repl_val, 0, sizeof(repl_val)); - memset(repl_null, false, sizeof(repl_null)); - memset(repl_repl, false, sizeof(repl_repl)); + /* - * If no indexes, or no columns are referenceable by FKs, we can avoid the - * scan. ++ * 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. + */ - if (RelationGetIndexList(partition) == NIL || - bms_is_empty(RelationGetIndexAttrBitmap(partition, - INDEX_ATTR_BITMAP_KEY))) - return NIL; - - /* Search for constraints referencing this table */ - pg_constraint = table_open(ConstraintRelationId, AccessShareLock); - ScanKeyInit(&key[0], - Anum_pg_constraint_confrelid, BTEqualStrategyNumber, - F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(partition))); - ScanKeyInit(&key[1], - Anum_pg_constraint_contype, BTEqualStrategyNumber, - F_CHAREQ, CharGetDatum(CONSTRAINT_FOREIGN)); - - /* XXX This is a seqscan, as we don't have a usable index */ - scan = systable_beginscan(pg_constraint, InvalidOid, true, NULL, 2, key); - while ((tuple = systable_getnext(scan)) != NULL) ++ if (IsA(stmt, RenameStmt)) + { - Form_pg_constraint constrForm = (Form_pg_constraint) GETSTRUCT(tuple); - - /* - * We only need to process constraints that are part of larger ones. - */ - if (!OidIsValid(constrForm->conparentid)) - continue; - - constraints = lappend_oid(constraints, constrForm->oid); ++ 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; - systable_endscan(scan); - table_close(pg_constraint, AccessShareLock); - - return constraints; - } - - /* - * During DETACH PARTITION, verify that any foreign keys pointing to the - * partitioned table would not become invalid. An error is raised if any - * referenced values exist. - */ - static void - ATDetachCheckNoForeignKeyRefs(Relation partition) - { - List *constraints; - ListCell *cell; - - constraints = GetParentedForeignKeyRefs(partition); - - foreach(cell, constraints) - /* Extract the current options */ - datum = SysCacheGetAttr(FOREIGNTABLEREL, - tuple, - Anum_pg_foreign_table_ftoptions, - &isnull); - if (isnull) - datum = PointerGetDatum(NULL); ++ else if (IsA(stmt, AlterTableStmt)) ++ reltype = ((AlterTableStmt *) stmt)->objtype; ++ else + { - Oid constrOid = lfirst_oid(cell); - HeapTuple tuple; - Form_pg_constraint constrForm; - Relation rel; - Trigger trig = {0}; ++ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(stmt)); ++ reltype = OBJECT_TABLE; /* placate compiler */ ++ } - tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid)); - if (!HeapTupleIsValid(tuple)) - elog(ERROR, "cache lookup failed for constraint %u", constrOid); - constrForm = (Form_pg_constraint) GETSTRUCT(tuple); - /* Transform the options */ - datum = transformGenericOptions(ForeignTableRelationId, - datum, - options, - fdw->fdwvalidator); ++ /* ++ * 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))); - Assert(OidIsValid(constrForm->conparentid)); - Assert(constrForm->confrelid == RelationGetRelid(partition)); - if (DatumGetPointer(datum) != NULL) - repl_val[Anum_pg_foreign_table_ftoptions - 1] = datum; - else - repl_null[Anum_pg_foreign_table_ftoptions - 1] = true; ++ if (reltype == OBJECT_VIEW && relkind != RELKIND_VIEW) ++ ereport(ERROR, ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("\"%s\" is not a view", rv->relname))); - /* prevent data changes into the referencing table until commit */ - rel = table_open(constrForm->conrelid, ShareLock); - repl_repl[Anum_pg_foreign_table_ftoptions - 1] = true; ++ if (reltype == OBJECT_MATVIEW && relkind != RELKIND_MATVIEW) ++ ereport(ERROR, ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("\"%s\" is not a materialized view", rv->relname))); - trig.tgoid = InvalidOid; - trig.tgname = NameStr(constrForm->conname); - trig.tgenabled = TRIGGER_FIRES_ON_ORIGIN; - trig.tgisinternal = true; - trig.tgconstrrelid = RelationGetRelid(partition); - trig.tgconstrindid = constrForm->conindid; - trig.tgconstraint = constrForm->oid; - trig.tgdeferrable = false; - trig.tginitdeferred = false; - /* we needn't fill in remaining fields */ - /* Everything looks good - update the tuple */ ++ 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))); - RI_PartitionRemove_Check(&trig, rel, partition); - tuple = heap_modify_tuple(tuple, RelationGetDescr(ftrel), - repl_val, repl_null, repl_repl); ++ if (reltype == OBJECT_TYPE && relkind != RELKIND_COMPOSITE_TYPE) ++ ereport(ERROR, ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("\"%s\" is not a composite type", rv->relname))); - ReleaseSysCache(tuple); - CatalogTupleUpdate(ftrel, &tuple->t_self, tuple); ++ 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))); - table_close(rel, NoLock); + /* - * Invalidate relcache so that all sessions will refresh any cached plans - * that might depend on the old options. ++ * Don't allow ALTER TABLE on composite types. We want people to use ALTER ++ * TYPE for that. + */ - CacheInvalidateRelcache(rel); - - InvokeObjectPostAlterHook(ForeignTableRelationId, - RelationGetRelid(rel), 0); ++ if (reltype != OBJECT_TYPE && relkind == RELKIND_COMPOSITE_TYPE) ++ ereport(ERROR, ++ (errcode(ERRCODE_WRONG_OBJECT_TYPE), ++ errmsg("\"%s\" is a composite type", rv->relname), ++ /* translator: %s is an SQL ALTER command */ ++ errhint("Use %s instead.", ++ "ALTER TYPE"))); + - table_close(ftrel, RowExclusiveLock); ++ /* ++ * 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."))); + } + - heap_freetuple(tuple); ++ ReleaseSysCache(tuple); } /*