1
0
mirror of https://github.com/postgres/postgres.git synced 2025-04-24 10:47:04 +03:00
Alexander Korotkov 885742b9f8 Change the way ATExecMergePartitions() handles the name collision
The name collision happens when the name of the new partition is the same as
the name of one of the merging partitions.  Currently, ATExecMergePartitions()
first gives the new partition a temporary name and then renames it when old
partitions are deleted.  That negatively influences the naming of related
objects like indexes and constrains, which could inherit a temporary name.

This commit changes the implementation in the following way.  A merging
partition gets renamed first, then the new partition is created with the
right name immediately.  This resolves the issue of the naming of related
objects.

Reported-by: Alexander Lakhin
Discussion: https://postgr.es/m/edfbd846-dcc1-42d1-ac26-715691b687d3%40postgrespro.ru
Author: Dmitry Koval, Alexander Korotkov
Reviewed-by: Robert Haas, Justin Pryzby, Pavel Borisov
2024-04-30 11:54:42 +03:00

21611 lines
667 KiB
C

/*-------------------------------------------------------------------------
*
* tablecmds.c
* Commands for creating and altering table structures and settings
*
* Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
*
* IDENTIFICATION
* src/backend/commands/tablecmds.c
*
*-------------------------------------------------------------------------
*/
#include "postgres.h"
#include "access/attmap.h"
#include "access/genam.h"
#include "access/gist.h"
#include "access/heapam.h"
#include "access/heapam_xlog.h"
#include "access/multixact.h"
#include "access/reloptions.h"
#include "access/relscan.h"
#include "access/sysattr.h"
#include "access/tableam.h"
#include "access/toast_compression.h"
#include "access/xact.h"
#include "access/xlog.h"
#include "access/xloginsert.h"
#include "catalog/catalog.h"
#include "catalog/heap.h"
#include "catalog/index.h"
#include "catalog/namespace.h"
#include "catalog/objectaccess.h"
#include "catalog/partition.h"
#include "catalog/pg_am.h"
#include "catalog/pg_attrdef.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_constraint.h"
#include "catalog/pg_depend.h"
#include "catalog/pg_foreign_table.h"
#include "catalog/pg_inherits.h"
#include "catalog/pg_largeobject.h"
#include "catalog/pg_namespace.h"
#include "catalog/pg_opclass.h"
#include "catalog/pg_policy.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_rewrite.h"
#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
#include "catalog/toasting.h"
#include "commands/cluster.h"
#include "commands/comment.h"
#include "commands/defrem.h"
#include "commands/event_trigger.h"
#include "commands/sequence.h"
#include "commands/tablecmds.h"
#include "commands/tablespace.h"
#include "commands/trigger.h"
#include "commands/typecmds.h"
#include "commands/user.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
#include "foreign/fdwapi.h"
#include "foreign/foreign.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "nodes/parsenodes.h"
#include "optimizer/optimizer.h"
#include "parser/parse_coerce.h"
#include "parser/parse_collate.h"
#include "parser/parse_expr.h"
#include "parser/parse_relation.h"
#include "parser/parse_type.h"
#include "parser/parse_utilcmd.h"
#include "parser/parser.h"
#include "partitioning/partbounds.h"
#include "partitioning/partdesc.h"
#include "pgstat.h"
#include "rewrite/rewriteDefine.h"
#include "rewrite/rewriteHandler.h"
#include "rewrite/rewriteManip.h"
#include "storage/bufmgr.h"
#include "storage/lmgr.h"
#include "storage/lock.h"
#include "storage/predicate.h"
#include "storage/smgr.h"
#include "tcop/utility.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
#include "utils/inval.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/partcache.h"
#include "utils/relcache.h"
#include "utils/ruleutils.h"
#include "utils/snapmgr.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
#include "utils/typcache.h"
#include "utils/usercontext.h"
/*
* ON COMMIT action list
*/
typedef struct OnCommitItem
{
Oid relid; /* relid of relation */
OnCommitAction oncommit; /* what to do at end of xact */
/*
* If this entry was created during the current transaction,
* creating_subid is the ID of the creating subxact; if created in a prior
* transaction, creating_subid is zero. If deleted during the current
* transaction, deleting_subid is the ID of the deleting subxact; if no
* deletion request is pending, deleting_subid is zero.
*/
SubTransactionId creating_subid;
SubTransactionId deleting_subid;
} OnCommitItem;
static List *on_commits = NIL;
/*
* State information for ALTER TABLE
*
* The pending-work queue for an ALTER TABLE is a List of AlteredTableInfo
* structs, one for each table modified by the operation (the named table
* plus any child tables that are affected). We save lists of subcommands
* to apply to this table (possibly modified by parse transformation steps);
* these lists will be executed in Phase 2. If a Phase 3 step is needed,
* necessary information is stored in the constraints and newvals lists.
*
* Phase 2 is divided into multiple passes; subcommands are executed in
* a pass determined by subcommand type.
*/
typedef enum AlterTablePass
{
AT_PASS_UNSET = -1, /* UNSET will cause ERROR */
AT_PASS_DROP, /* DROP (all flavors) */
AT_PASS_ALTER_TYPE, /* ALTER COLUMN TYPE */
AT_PASS_ADD_COL, /* ADD COLUMN */
AT_PASS_SET_EXPRESSION, /* ALTER SET EXPRESSION */
AT_PASS_OLD_COL_ATTRS, /* re-install attnotnull */
AT_PASS_OLD_INDEX, /* re-add existing indexes */
AT_PASS_OLD_CONSTR, /* re-add existing constraints */
/* We could support a RENAME COLUMN pass here, but not currently used */
AT_PASS_ADD_CONSTR, /* ADD constraints (initial examination) */
AT_PASS_COL_ATTRS, /* set column attributes, eg NOT NULL */
AT_PASS_ADD_INDEXCONSTR, /* ADD index-based constraints */
AT_PASS_ADD_INDEX, /* ADD indexes */
AT_PASS_ADD_OTHERCONSTR, /* ADD other constraints, defaults */
AT_PASS_MISC, /* other stuff */
} AlterTablePass;
#define AT_NUM_PASSES (AT_PASS_MISC + 1)
typedef struct AlteredTableInfo
{
/* Information saved before any work commences: */
Oid relid; /* Relation to work on */
char relkind; /* Its relkind */
TupleDesc oldDesc; /* Pre-modification tuple descriptor */
/*
* Transiently set during Phase 2, normally set to NULL.
*
* ATRewriteCatalogs sets this when it starts, and closes when ATExecCmd
* returns control. This can be exploited by ATExecCmd subroutines to
* close/reopen across transaction boundaries.
*/
Relation rel;
/* Information saved by Phase 1 for Phase 2: */
List *subcmds[AT_NUM_PASSES]; /* Lists of AlterTableCmd */
/* Information saved by Phases 1/2 for Phase 3: */
List *constraints; /* List of NewConstraint */
List *newvals; /* List of NewColumnValue */
List *afterStmts; /* List of utility command parsetrees */
bool verify_new_notnull; /* T if we should recheck NOT NULL */
int rewrite; /* Reason for forced rewrite, if any */
bool chgAccessMethod; /* T if SET ACCESS METHOD is used */
Oid newAccessMethod; /* new access method; 0 means no change,
* if above is true */
Oid newTableSpace; /* new tablespace; 0 means no change */
bool chgPersistence; /* T if SET LOGGED/UNLOGGED is used */
char newrelpersistence; /* if above is true */
Expr *partition_constraint; /* for attach partition validation */
/* true, if validating default due to some other attach/detach */
bool validate_default;
/* Objects to rebuild after completing ALTER TYPE operations */
List *changedConstraintOids; /* OIDs of constraints to rebuild */
List *changedConstraintDefs; /* string definitions of same */
List *changedIndexOids; /* OIDs of indexes to rebuild */
List *changedIndexDefs; /* string definitions of same */
char *replicaIdentityIndex; /* index to reset as REPLICA IDENTITY */
char *clusterOnIndex; /* index to use for CLUSTER */
List *changedStatisticsOids; /* OIDs of statistics to rebuild */
List *changedStatisticsDefs; /* string definitions of same */
} AlteredTableInfo;
/* Struct describing one new constraint to check in Phase 3 scan */
/* Note: new not-null constraints are handled elsewhere */
typedef struct NewConstraint
{
char *name; /* Constraint name, or NULL if none */
ConstrType contype; /* CHECK or FOREIGN */
Oid refrelid; /* PK rel, if FOREIGN */
Oid refindid; /* OID of PK's index, if FOREIGN */
bool conwithperiod; /* Whether the new FOREIGN KEY uses PERIOD */
Oid conid; /* OID of pg_constraint entry, if FOREIGN */
Node *qual; /* Check expr or CONSTR_FOREIGN Constraint */
ExprState *qualstate; /* Execution state for CHECK expr */
} NewConstraint;
/*
* Struct describing one new column value that needs to be computed during
* Phase 3 copy (this could be either a new column with a non-null default, or
* a column that we're changing the type of). Columns without such an entry
* are just copied from the old table during ATRewriteTable. Note that the
* expr is an expression over *old* table values, except when is_generated
* is true; then it is an expression over columns of the *new* tuple.
*/
typedef struct NewColumnValue
{
AttrNumber attnum; /* which column */
Expr *expr; /* expression to compute */
ExprState *exprstate; /* execution state */
bool is_generated; /* is it a GENERATED expression? */
} NewColumnValue;
/*
* Error-reporting support for RemoveRelations
*/
struct dropmsgstrings
{
char kind;
int nonexistent_code;
const char *nonexistent_msg;
const char *skipping_msg;
const char *nota_msg;
const char *drophint_msg;
};
static const struct dropmsgstrings dropmsgstringarray[] = {
{RELKIND_RELATION,
ERRCODE_UNDEFINED_TABLE,
gettext_noop("table \"%s\" does not exist"),
gettext_noop("table \"%s\" does not exist, skipping"),
gettext_noop("\"%s\" is not a table"),
gettext_noop("Use DROP TABLE to remove a table.")},
{RELKIND_SEQUENCE,
ERRCODE_UNDEFINED_TABLE,
gettext_noop("sequence \"%s\" does not exist"),
gettext_noop("sequence \"%s\" does not exist, skipping"),
gettext_noop("\"%s\" is not a sequence"),
gettext_noop("Use DROP SEQUENCE to remove a sequence.")},
{RELKIND_VIEW,
ERRCODE_UNDEFINED_TABLE,
gettext_noop("view \"%s\" does not exist"),
gettext_noop("view \"%s\" does not exist, skipping"),
gettext_noop("\"%s\" is not a view"),
gettext_noop("Use DROP VIEW to remove a view.")},
{RELKIND_MATVIEW,
ERRCODE_UNDEFINED_TABLE,
gettext_noop("materialized view \"%s\" does not exist"),
gettext_noop("materialized view \"%s\" does not exist, skipping"),
gettext_noop("\"%s\" is not a materialized view"),
gettext_noop("Use DROP MATERIALIZED VIEW to remove a materialized view.")},
{RELKIND_INDEX,
ERRCODE_UNDEFINED_OBJECT,
gettext_noop("index \"%s\" does not exist"),
gettext_noop("index \"%s\" does not exist, skipping"),
gettext_noop("\"%s\" is not an index"),
gettext_noop("Use DROP INDEX to remove an index.")},
{RELKIND_COMPOSITE_TYPE,
ERRCODE_UNDEFINED_OBJECT,
gettext_noop("type \"%s\" does not exist"),
gettext_noop("type \"%s\" does not exist, skipping"),
gettext_noop("\"%s\" is not a type"),
gettext_noop("Use DROP TYPE to remove a type.")},
{RELKIND_FOREIGN_TABLE,
ERRCODE_UNDEFINED_OBJECT,
gettext_noop("foreign table \"%s\" does not exist"),
gettext_noop("foreign table \"%s\" does not exist, skipping"),
gettext_noop("\"%s\" is not a foreign table"),
gettext_noop("Use DROP FOREIGN TABLE to remove a foreign table.")},
{RELKIND_PARTITIONED_TABLE,
ERRCODE_UNDEFINED_TABLE,
gettext_noop("table \"%s\" does not exist"),
gettext_noop("table \"%s\" does not exist, skipping"),
gettext_noop("\"%s\" is not a table"),
gettext_noop("Use DROP TABLE to remove a table.")},
{RELKIND_PARTITIONED_INDEX,
ERRCODE_UNDEFINED_OBJECT,
gettext_noop("index \"%s\" does not exist"),
gettext_noop("index \"%s\" does not exist, skipping"),
gettext_noop("\"%s\" is not an index"),
gettext_noop("Use DROP INDEX to remove an index.")},
{'\0', 0, NULL, NULL, NULL, NULL}
};
/* communication between RemoveRelations and RangeVarCallbackForDropRelation */
struct DropRelationCallbackState
{
/* These fields are set by RemoveRelations: */
char expected_relkind;
LOCKMODE heap_lockmode;
/* These fields are state to track which subsidiary locks are held: */
Oid heapOid;
Oid partParentOid;
/* These fields are passed back by RangeVarCallbackForDropRelation: */
char actual_relkind;
char actual_relpersistence;
};
/* Alter table target-type flags for ATSimplePermissions */
#define ATT_TABLE 0x0001
#define ATT_VIEW 0x0002
#define ATT_MATVIEW 0x0004
#define ATT_INDEX 0x0008
#define ATT_COMPOSITE_TYPE 0x0010
#define ATT_FOREIGN_TABLE 0x0020
#define ATT_PARTITIONED_INDEX 0x0040
#define ATT_SEQUENCE 0x0080
/*
* ForeignTruncateInfo
*
* Information related to truncation of foreign tables. This is used for
* the elements in a hash table. It uses the server OID as lookup key,
* and includes a per-server list of all foreign tables involved in the
* truncation.
*/
typedef struct ForeignTruncateInfo
{
Oid serverid;
List *rels;
} ForeignTruncateInfo;
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
* Otherwise, for regular inheritance use NORMAL dependency.
*/
#define child_dependency_type(child_is_partition) \
((child_is_partition) ? DEPENDENCY_AUTO : DEPENDENCY_NORMAL)
static void truncate_check_rel(Oid relid, Form_pg_class reltuple);
static void truncate_check_perms(Oid relid, Form_pg_class reltuple);
static void truncate_check_activity(Relation rel);
static void RangeVarCallbackForTruncate(const RangeVar *relation,
Oid relId, Oid oldRelId, void *arg);
static List *MergeAttributes(List *columns, const List *supers, char relpersistence,
bool is_partition, List **supconstr,
List **supnotnulls);
static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr);
static void MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const ColumnDef *newdef);
static ColumnDef *MergeInheritedAttribute(List *inh_columns, int exist_attno, const ColumnDef *newdef);
static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispartition);
static void MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel);
static void StoreCatalogInheritance(Oid relationId, List *supers,
bool child_is_partition);
static void StoreCatalogInheritance1(Oid relationId, Oid parentOid,
int32 seqNumber, Relation inhRelation,
bool child_is_partition);
static int findAttrByName(const char *attributeName, const List *columns);
static void AlterIndexNamespaces(Relation classRel, Relation rel,
Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved);
static void AlterSeqNamespaces(Relation classRel, Relation rel,
Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved,
LOCKMODE lockmode);
static ObjectAddress ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
static bool ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
Relation rel, HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode);
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
static int transformColumnNameList(Oid relId, List *colList,
int16 *attnums, Oid *atttypids);
static int transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
List **attnamelist,
int16 *attnums, Oid *atttypids,
Oid *opclasses, bool *pk_has_without_overlaps);
static Oid transformFkeyCheckAttrs(Relation pkrel,
int numattrs, int16 *attnums,
bool with_period, Oid *opclasses,
bool *pk_has_without_overlaps);
static void checkFkeyPermissions(Relation rel, int16 *attnums, int natts);
static CoercionPathType findFkeyCast(Oid targetTypeId, Oid sourceTypeId,
Oid *funcid);
static void validateForeignKeyConstraint(char *conname,
Relation rel, Relation pkrel,
Oid pkindOid, Oid constraintOid, bool hasperiod);
static void ATController(AlterTableStmt *parsetree,
Relation rel, List *cmds, bool recurse, LOCKMODE lockmode,
AlterTableUtilityContext *context);
static void ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode,
AlterTableUtilityContext *context);
static void ATRewriteCatalogs(List **wqueue, LOCKMODE lockmode,
AlterTableUtilityContext *context);
static void ATExecCmd(List **wqueue, AlteredTableInfo *tab,
AlterTableCmd *cmd, LOCKMODE lockmode, AlterTablePass cur_pass,
AlterTableUtilityContext *context);
static AlterTableCmd *ATParseTransformCmd(List **wqueue, AlteredTableInfo *tab,
Relation rel, AlterTableCmd *cmd,
bool recurse, LOCKMODE lockmode,
AlterTablePass cur_pass,
AlterTableUtilityContext *context);
static void ATRewriteTables(AlterTableStmt *parsetree,
List **wqueue, LOCKMODE lockmode,
AlterTableUtilityContext *context);
static void ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode);
static AlteredTableInfo *ATGetQueueEntry(List **wqueue, Relation rel);
static void ATSimplePermissions(AlterTableType cmdtype, Relation rel, int allowed_targets);
static void ATSimpleRecursion(List **wqueue, Relation rel,
AlterTableCmd *cmd, bool recurse, LOCKMODE lockmode,
AlterTableUtilityContext *context);
static void ATCheckPartitionsNotInUse(Relation rel, LOCKMODE lockmode);
static void ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd,
LOCKMODE lockmode,
AlterTableUtilityContext *context);
static List *find_typed_table_dependencies(Oid typeOid, const char *typeName,
DropBehavior behavior);
static void ATPrepAddColumn(List **wqueue, Relation rel, bool recurse, bool recursing,
bool is_view, AlterTableCmd *cmd, LOCKMODE lockmode,
AlterTableUtilityContext *context);
static ObjectAddress ATExecAddColumn(List **wqueue, AlteredTableInfo *tab,
Relation rel, AlterTableCmd **cmd,
bool recurse, bool recursing,
LOCKMODE lockmode, AlterTablePass cur_pass,
AlterTableUtilityContext *context);
static bool check_for_column_name_collision(Relation rel, const char *colname,
bool if_not_exists);
static void add_column_datatype_dependency(Oid relid, int32 attnum, Oid typid);
static void add_column_collation_dependency(Oid relid, int32 attnum, Oid collid);
static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
LOCKMODE lockmode);
static bool set_attnotnull(List **wqueue, Relation rel,
AttrNumber attnum, bool recurse, LOCKMODE lockmode);
static ObjectAddress ATExecSetNotNull(List **wqueue, Relation rel,
char *constrname, char *colName,
bool recurse, bool recursing,
List **readyRels, LOCKMODE lockmode);
static ObjectAddress ATExecSetAttNotNull(List **wqueue, Relation rel,
const char *colName, LOCKMODE lockmode);
static bool NotNullImpliedByRelConstraints(Relation rel, Form_pg_attribute attr);
static bool ConstraintImpliedByRelConstraint(Relation scanrel,
List *testConstraint, List *provenConstraint);
static ObjectAddress ATExecColumnDefault(Relation rel, const char *colName,
Node *newDefault, LOCKMODE lockmode);
static ObjectAddress ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
Node *newDefault);
static ObjectAddress ATExecAddIdentity(Relation rel, const char *colName,
Node *def, LOCKMODE lockmode, bool recurse, bool recursing);
static ObjectAddress ATExecSetIdentity(Relation rel, const char *colName,
Node *def, LOCKMODE lockmode, bool recurse, bool recursing);
static ObjectAddress ATExecDropIdentity(Relation rel, const char *colName, bool missing_ok, LOCKMODE lockmode,
bool recurse, bool recursing);
static ObjectAddress ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
Node *newExpr, LOCKMODE lockmode);
static void ATPrepDropExpression(Relation rel, AlterTableCmd *cmd, bool recurse, bool recursing, LOCKMODE lockmode);
static ObjectAddress ATExecDropExpression(Relation rel, const char *colName, bool missing_ok, LOCKMODE lockmode);
static ObjectAddress ATExecSetStatistics(Relation rel, const char *colName, int16 colNum,
Node *newValue, LOCKMODE lockmode);
static ObjectAddress ATExecSetOptions(Relation rel, const char *colName,
Node *options, bool isReset, LOCKMODE lockmode);
static ObjectAddress ATExecSetStorage(Relation rel, const char *colName,
Node *newValue, LOCKMODE lockmode);
static void ATPrepDropColumn(List **wqueue, Relation rel, bool recurse, bool recursing,
AlterTableCmd *cmd, LOCKMODE lockmode,
AlterTableUtilityContext *context);
static ObjectAddress ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
DropBehavior behavior,
bool recurse, bool recursing,
bool missing_ok, LOCKMODE lockmode,
ObjectAddresses *addrs);
static void ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
LOCKMODE lockmode, AlterTableUtilityContext *context);
static ObjectAddress ATExecAddIndex(AlteredTableInfo *tab, Relation rel,
IndexStmt *stmt, bool is_rebuild, LOCKMODE lockmode);
static ObjectAddress ATExecAddStatistics(AlteredTableInfo *tab, Relation rel,
CreateStatsStmt *stmt, bool is_rebuild, LOCKMODE lockmode);
static ObjectAddress ATExecAddConstraint(List **wqueue,
AlteredTableInfo *tab, Relation rel,
Constraint *newConstraint, bool recurse, bool is_readd,
LOCKMODE lockmode);
static char *ChooseForeignKeyConstraintNameAddition(List *colnames);
static ObjectAddress ATExecAddIndexConstraint(AlteredTableInfo *tab, Relation rel,
IndexStmt *stmt, LOCKMODE lockmode);
static ObjectAddress ATAddCheckNNConstraint(List **wqueue,
AlteredTableInfo *tab, Relation rel,
Constraint *constr,
bool recurse, bool recursing, bool is_readd,
LOCKMODE lockmode);
static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab,
Relation rel, Constraint *fkconstraint,
bool recurse, bool recursing,
LOCKMODE lockmode);
static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
int numfkdelsetcols, int16 *fkdelsetcols,
bool old_check_ok,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period);
static void validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
int numfksetcols, const int16 *fksetcolsattnums,
List *fksetcols);
static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
int numfkdelsetcols, int16 *fkdelsetcols,
bool old_check_ok, LOCKMODE lockmode,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period);
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
static void CloneFkReferencing(List **wqueue, Relation parentRel,
Relation partRel);
static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
Oid indexOid,
Oid parentInsTrigger, Oid parentUpdTrigger,
Oid *insertTrigOid, Oid *updateTrigOid);
static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
Oid indexOid,
Oid parentDelTrigger, Oid parentUpdTrigger,
Oid *deleteTrigOid, Oid *updateTrigOid);
static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
Oid partRelid,
Oid parentConstrOid, int numfks,
AttrNumber *mapped_conkey, AttrNumber *confkey,
Oid *conpfeqop,
Oid parentInsTrigger,
Oid parentUpdTrigger,
Relation trigrel);
static void GetForeignKeyActionTriggers(Relation trigrel,
Oid conoid, Oid confrelid, Oid conrelid,
Oid *deleteTriggerOid,
Oid *updateTriggerOid);
static void GetForeignKeyCheckTriggers(Relation trigrel,
Oid conoid, Oid confrelid, Oid conrelid,
Oid *insertTriggerOid,
Oid *updateTriggerOid);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior, bool recurse,
bool missing_ok, LOCKMODE lockmode);
static ObjectAddress dropconstraint_internal(Relation rel,
HeapTuple constraintTup, DropBehavior behavior,
bool recurse, bool recursing,
bool missing_ok, List **readyRels,
LOCKMODE lockmode);
static void ATPrepAlterColumnType(List **wqueue,
AlteredTableInfo *tab, Relation rel,
bool recurse, bool recursing,
AlterTableCmd *cmd, LOCKMODE lockmode,
AlterTableUtilityContext *context);
static bool ATColumnChangeRequiresRewrite(Node *expr, AttrNumber varattno);
static ObjectAddress ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
AlterTableCmd *cmd, LOCKMODE lockmode);
static void RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
Relation rel, AttrNumber attnum, const char *colName);
static void RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab);
static void RememberIndexForRebuilding(Oid indoid, AlteredTableInfo *tab);
static void RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab);
static void ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab,
LOCKMODE lockmode);
static void ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId,
char *cmd, List **wqueue, LOCKMODE lockmode,
bool rewrite);
static void RebuildConstraintComment(AlteredTableInfo *tab, AlterTablePass pass,
Oid objid, Relation rel, List *domname,
const char *conname);
static void TryReuseIndex(Oid oldId, IndexStmt *stmt);
static void TryReuseForeignKey(Oid oldId, Constraint *con);
static ObjectAddress ATExecAlterColumnGenericOptions(Relation rel, const char *colName,
List *options, LOCKMODE lockmode);
static void change_owner_fix_column_acls(Oid relationOid,
Oid oldOwnerId, Oid newOwnerId);
static void change_owner_recurse_to_sequences(Oid relationOid,
Oid newOwnerId, LOCKMODE lockmode);
static ObjectAddress ATExecClusterOn(Relation rel, const char *indexName,
LOCKMODE lockmode);
static void ATExecDropCluster(Relation rel, LOCKMODE lockmode);
static void ATPrepSetAccessMethod(AlteredTableInfo *tab, Relation rel, const char *amname);
static void ATExecSetAccessMethodNoStorage(Relation rel, Oid newAccessMethod);
static bool ATPrepChangePersistence(Relation rel, bool toLogged);
static void ATPrepSetTableSpace(AlteredTableInfo *tab, Relation rel,
const char *tablespacename, LOCKMODE lockmode);
static void ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode);
static void ATExecSetTableSpaceNoStorage(Relation rel, Oid newTableSpace);
static void ATExecSetRelOptions(Relation rel, List *defList,
AlterTableType operation,
LOCKMODE lockmode);
static void ATExecEnableDisableTrigger(Relation rel, const char *trigname,
char fires_when, bool skip_system, bool recurse,
LOCKMODE lockmode);
static void ATExecEnableDisableRule(Relation rel, const char *rulename,
char fires_when, LOCKMODE lockmode);
static void ATPrepAddInherit(Relation child_rel);
static ObjectAddress ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode);
static ObjectAddress ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode);
static void drop_parent_dependency(Oid relid, Oid refclassid, Oid refobjid,
DependencyType deptype);
static ObjectAddress ATExecAddOf(Relation rel, const TypeName *ofTypename, LOCKMODE lockmode);
static void ATExecDropOf(Relation rel, LOCKMODE lockmode);
static void ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode);
static void ATExecGenericOptions(Relation rel, List *options);
static void ATExecSetRowSecurity(Relation rel, bool rls);
static void ATExecForceNoForceRowSecurity(Relation rel, bool force_rls);
static ObjectAddress ATExecSetCompression(Relation rel,
const char *column, Node *newValue, LOCKMODE lockmode);
static void index_copy_data(Relation rel, RelFileLocator newrlocator);
static const char *storage_name(char c);
static void RangeVarCallbackForDropRelation(const RangeVar *rel, Oid relOid,
Oid oldRelOid, void *arg);
static void RangeVarCallbackForAlterRelation(const RangeVar *rv, Oid relid,
Oid oldrelid, void *arg);
static PartitionSpec *transformPartitionSpec(Relation rel, PartitionSpec *partspec);
static void ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNumber *partattrs,
List **partexprs, Oid *partopclass, Oid *partcollation,
PartitionStrategy strategy);
static void CreateInheritance(Relation child_rel, Relation parent_rel, bool ispartition);
static void RemoveInheritance(Relation child_rel, Relation parent_rel,
bool expect_detached);
static void ATInheritAdjustNotNulls(Relation parent_rel, Relation child_rel,
int inhcount);
static ObjectAddress ATExecAttachPartition(List **wqueue, Relation rel,
PartitionCmd *cmd,
AlterTableUtilityContext *context);
static void AttachPartitionEnsureIndexes(List **wqueue, Relation rel, Relation attachrel);
static void QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
List *partConstraint,
bool validate_default);
static void CloneRowTriggersToPartition(Relation parent, Relation partition);
static void DetachAddConstraintIfNeeded(List **wqueue, Relation partRel);
static void DropClonedTriggersFromPartition(Oid partitionId);
static ObjectAddress ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab,
Relation rel, RangeVar *name,
bool concurrent);
static void DetachPartitionFinalize(Relation rel, Relation partRel,
bool concurrent, Oid defaultPartOid);
static ObjectAddress ATExecDetachPartitionFinalize(Relation rel, RangeVar *name);
static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx,
RangeVar *name);
static void validatePartitionedIndex(Relation partedIdx, Relation partedTbl);
static void refuseDupeIndexAttach(Relation parentIdx, Relation partIdx,
Relation partitionTbl);
static void verifyPartitionIndexNotNull(IndexInfo *iinfo, Relation partIdx);
static List *GetParentedForeignKeyRefs(Relation partition);
static void ATDetachCheckNoForeignKeyRefs(Relation partition);
static char GetAttributeCompression(Oid atttypid, const char *compression);
static char GetAttributeStorage(Oid atttypid, const char *storagemode);
static void ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab,
Relation rel, PartitionCmd *cmd,
AlterTableUtilityContext *context);
static void ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
PartitionCmd *cmd, AlterTableUtilityContext *context);
/* ----------------------------------------------------------------
* DefineRelation
* Creates a new relation.
*
* stmt carries parsetree information from an ordinary CREATE TABLE statement.
* The other arguments are used to extend the behavior for other cases:
* relkind: relkind to assign to the new relation
* ownerId: if not InvalidOid, use this as the new relation's owner.
* typaddress: if not null, it's set to the pg_type entry's address.
* queryString: for error reporting
*
* Note that permissions checks are done against current user regardless of
* ownerId. A nonzero ownerId is used when someone is creating a relation
* "on behalf of" someone else, so we still want to see that the current user
* has permissions to do it.
*
* If successful, returns the address of the new relation.
* ----------------------------------------------------------------
*/
ObjectAddress
DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
ObjectAddress *typaddress, const char *queryString)
{
char relname[NAMEDATALEN];
Oid namespaceId;
Oid relationId;
Oid tablespaceId;
Relation rel;
TupleDesc descriptor;
List *inheritOids;
List *old_constraints;
List *old_notnulls;
List *rawDefaults;
List *cookedDefaults;
List *nncols;
Datum reloptions;
ListCell *listptr;
AttrNumber attnum;
bool partitioned;
static char *validnsps[] = HEAP_RELOPT_NAMESPACES;
Oid ofTypeId;
ObjectAddress address;
LOCKMODE parentLockmode;
Oid accessMethodId = InvalidOid;
/*
* Truncate relname to appropriate length (probably a waste of time, as
* parser should have done this already).
*/
strlcpy(relname, stmt->relation->relname, NAMEDATALEN);
/*
* Check consistency of arguments
*/
if (stmt->oncommit != ONCOMMIT_NOOP
&& stmt->relation->relpersistence != RELPERSISTENCE_TEMP)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("ON COMMIT can only be used on temporary tables")));
if (stmt->partspec != NULL)
{
if (relkind != RELKIND_RELATION)
elog(ERROR, "unexpected relkind: %d", (int) relkind);
relkind = RELKIND_PARTITIONED_TABLE;
partitioned = true;
}
else
partitioned = false;
/*
* Look up the namespace in which we are supposed to create the relation,
* check we have permission to create there, lock it against concurrent
* drop, and mark stmt->relation as RELPERSISTENCE_TEMP if a temporary
* namespace is selected.
*/
namespaceId =
RangeVarGetAndCheckCreationNamespace(stmt->relation, NoLock, NULL);
/*
* Security check: disallow creating temp tables from security-restricted
* code. This is needed because calling code might not expect untrusted
* tables to appear in pg_temp at the front of its search path.
*/
if (stmt->relation->relpersistence == RELPERSISTENCE_TEMP
&& InSecurityRestrictedOperation())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("cannot create temporary table within security-restricted operation")));
/*
* Determine the lockmode to use when scanning parents. A self-exclusive
* lock is needed here.
*
* For regular inheritance, if two backends attempt to add children to the
* same parent simultaneously, and that parent has no pre-existing
* children, then both will attempt to update the parent's relhassubclass
* field, leading to a "tuple concurrently updated" error. Also, this
* interlocks against a concurrent ANALYZE on the parent table, which
* might otherwise be attempting to clear the parent's relhassubclass
* field, if its previous children were recently dropped.
*
* If the child table is a partition, then we instead grab an exclusive
* lock on the parent because its partition descriptor will be changed by
* addition of the new partition.
*/
parentLockmode = (stmt->partbound != NULL ? AccessExclusiveLock :
ShareUpdateExclusiveLock);
/* Determine the list of OIDs of the parents. */
inheritOids = NIL;
foreach(listptr, stmt->inhRelations)
{
RangeVar *rv = (RangeVar *) lfirst(listptr);
Oid parentOid;
parentOid = RangeVarGetRelid(rv, parentLockmode, false);
/*
* Reject duplications in the list of parents.
*/
if (list_member_oid(inheritOids, parentOid))
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_TABLE),
errmsg("relation \"%s\" would be inherited from more than once",
get_rel_name(parentOid))));
inheritOids = lappend_oid(inheritOids, parentOid);
}
/*
* Select tablespace to use: an explicitly indicated one, or (in the case
* of a partitioned table) the parent's, if it has one.
*/
if (stmt->tablespacename)
{
tablespaceId = get_tablespace_oid(stmt->tablespacename, false);
if (partitioned && tablespaceId == MyDatabaseTableSpace)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot specify default tablespace for partitioned relations")));
}
else if (stmt->partbound)
{
Assert(list_length(inheritOids) == 1);
tablespaceId = get_rel_tablespace(linitial_oid(inheritOids));
}
else
tablespaceId = InvalidOid;
/* still nothing? use the default */
if (!OidIsValid(tablespaceId))
tablespaceId = GetDefaultTablespace(stmt->relation->relpersistence,
partitioned);
/* Check permissions except when using database's default */
if (OidIsValid(tablespaceId) && tablespaceId != MyDatabaseTableSpace)
{
AclResult aclresult;
aclresult = object_aclcheck(TableSpaceRelationId, tablespaceId, GetUserId(),
ACL_CREATE);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult, OBJECT_TABLESPACE,
get_tablespace_name(tablespaceId));
}
/* In all cases disallow placing user relations in pg_global */
if (tablespaceId == GLOBALTABLESPACE_OID)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("only shared relations can be placed in pg_global tablespace")));
/* Identify user ID that will own the table */
if (!OidIsValid(ownerId))
ownerId = GetUserId();
/*
* Parse and validate reloptions, if any.
*/
reloptions = transformRelOptions((Datum) 0, stmt->options, NULL, validnsps,
true, false);
switch (relkind)
{
case RELKIND_VIEW:
(void) view_reloptions(reloptions, true);
break;
case RELKIND_PARTITIONED_TABLE:
(void) partitioned_table_reloptions(reloptions, true);
break;
default:
(void) heap_reloptions(relkind, reloptions, true);
}
if (stmt->ofTypename)
{
AclResult aclresult;
ofTypeId = typenameTypeId(NULL, stmt->ofTypename);
aclresult = object_aclcheck(TypeRelationId, ofTypeId, GetUserId(), ACL_USAGE);
if (aclresult != ACLCHECK_OK)
aclcheck_error_type(aclresult, ofTypeId);
}
else
ofTypeId = InvalidOid;
/*
* Look up inheritance ancestors and generate relation schema, including
* inherited attributes. (Note that stmt->tableElts is destructively
* modified by MergeAttributes.)
*/
stmt->tableElts =
MergeAttributes(stmt->tableElts, inheritOids,
stmt->relation->relpersistence,
stmt->partbound != NULL,
&old_constraints, &old_notnulls);
/*
* Create a tuple descriptor from the relation schema. Note that this
* deals with column names, types, and in-descriptor NOT NULL flags, but
* not default values, NOT NULL or CHECK constraints; we handle those
* below.
*/
descriptor = BuildDescForRelation(stmt->tableElts);
/*
* Find columns with default values and prepare for insertion of the
* defaults. Pre-cooked (that is, inherited) defaults go into a list of
* CookedConstraint structs that we'll pass to heap_create_with_catalog,
* while raw defaults go into a list of RawColumnDefault structs that will
* be processed by AddRelationNewConstraints. (We can't deal with raw
* expressions until we can do transformExpr.)
*
* We can set the atthasdef flags now in the tuple descriptor; this just
* saves StoreAttrDefault from having to do an immediate update of the
* pg_attribute rows.
*/
rawDefaults = NIL;
cookedDefaults = NIL;
attnum = 0;
foreach(listptr, stmt->tableElts)
{
ColumnDef *colDef = lfirst(listptr);
Form_pg_attribute attr;
attnum++;
attr = TupleDescAttr(descriptor, attnum - 1);
if (colDef->raw_default != NULL)
{
RawColumnDefault *rawEnt;
Assert(colDef->cooked_default == NULL);
rawEnt = (RawColumnDefault *) palloc(sizeof(RawColumnDefault));
rawEnt->attnum = attnum;
rawEnt->raw_default = colDef->raw_default;
rawEnt->missingMode = false;
rawEnt->generated = colDef->generated;
rawDefaults = lappend(rawDefaults, rawEnt);
attr->atthasdef = true;
}
else if (colDef->cooked_default != NULL)
{
CookedConstraint *cooked;
cooked = (CookedConstraint *) palloc(sizeof(CookedConstraint));
cooked->contype = CONSTR_DEFAULT;
cooked->conoid = InvalidOid; /* until created */
cooked->name = NULL;
cooked->attnum = attnum;
cooked->expr = colDef->cooked_default;
cooked->skip_validation = false;
cooked->is_local = true; /* not used for defaults */
cooked->inhcount = 0; /* ditto */
cooked->is_no_inherit = false;
cookedDefaults = lappend(cookedDefaults, cooked);
attr->atthasdef = true;
}
}
/*
* For relations with table AM and partitioned tables, select access
* method to use: an explicitly indicated one, or (in the case of a
* partitioned table) the parent's, if it has one.
*/
if (stmt->accessMethod != NULL)
{
Assert(RELKIND_HAS_TABLE_AM(relkind) || relkind == RELKIND_PARTITIONED_TABLE);
accessMethodId = get_table_am_oid(stmt->accessMethod, false);
}
else if (RELKIND_HAS_TABLE_AM(relkind) || relkind == RELKIND_PARTITIONED_TABLE)
{
if (stmt->partbound)
{
Assert(list_length(inheritOids) == 1);
accessMethodId = get_rel_relam(linitial_oid(inheritOids));
}
if (RELKIND_HAS_TABLE_AM(relkind) && !OidIsValid(accessMethodId))
accessMethodId = get_table_am_oid(default_table_access_method, false);
}
/*
* Create the relation. Inherited defaults and constraints are passed in
* for immediate handling --- since they don't need parsing, they can be
* stored immediately.
*/
relationId = heap_create_with_catalog(relname,
namespaceId,
tablespaceId,
InvalidOid,
InvalidOid,
ofTypeId,
ownerId,
accessMethodId,
descriptor,
list_concat(cookedDefaults,
old_constraints),
relkind,
stmt->relation->relpersistence,
false,
false,
stmt->oncommit,
reloptions,
true,
allowSystemTableMods,
false,
InvalidOid,
typaddress);
/*
* We must bump the command counter to make the newly-created relation
* tuple visible for opening.
*/
CommandCounterIncrement();
/*
* Open the new relation and acquire exclusive lock on it. This isn't
* really necessary for locking out other backends (since they can't see
* the new rel anyway until we commit), but it keeps the lock manager from
* complaining about deadlock risks.
*/
rel = relation_open(relationId, AccessExclusiveLock);
/*
* Now add any newly specified column default and generation expressions
* to the new relation. These are passed to us in the form of raw
* parsetrees; we need to transform them to executable expression trees
* before they can be added. The most convenient way to do that is to
* apply the parser's transformExpr routine, but transformExpr doesn't
* work unless we have a pre-existing relation. So, the transformation has
* to be postponed to this final step of CREATE TABLE.
*
* This needs to be before processing the partitioning clauses because
* those could refer to generated columns.
*/
if (rawDefaults)
AddRelationNewConstraints(rel, rawDefaults, NIL,
true, true, false, queryString);
/*
* Make column generation expressions visible for use by partitioning.
*/
CommandCounterIncrement();
/* Process and store partition bound, if any. */
if (stmt->partbound)
{
PartitionBoundSpec *bound;
ParseState *pstate;
Oid parentId = linitial_oid(inheritOids),
defaultPartOid;
Relation parent,
defaultRel = NULL;
ParseNamespaceItem *nsitem;
/* Already have strong enough lock on the parent */
parent = table_open(parentId, NoLock);
/*
* We are going to try to validate the partition bound specification
* against the partition key of parentRel, so it better have one.
*/
if (parent->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("\"%s\" is not partitioned",
RelationGetRelationName(parent))));
/*
* The partition constraint of the default partition depends on the
* partition bounds of every other partition. It is possible that
* another backend might be about to execute a query on the default
* partition table, and that the query relies on previously cached
* default partition constraints. We must therefore take a table lock
* strong enough to prevent all queries on the default partition from
* proceeding until we commit and send out a shared-cache-inval notice
* that will make them update their index lists.
*
* Order of locking: The relation being added won't be visible to
* other backends until it is committed, hence here in
* DefineRelation() the order of locking the default partition and the
* relation being added does not matter. But at all other places we
* need to lock the default relation before we lock the relation being
* added or removed i.e. we should take the lock in same order at all
* the places such that lock parent, lock default partition and then
* lock the partition so as to avoid a deadlock.
*/
defaultPartOid =
get_default_oid_from_partdesc(RelationGetPartitionDesc(parent,
true));
if (OidIsValid(defaultPartOid))
defaultRel = table_open(defaultPartOid, AccessExclusiveLock);
/* Transform the bound values */
pstate = make_parsestate(NULL);
pstate->p_sourcetext = queryString;
/*
* Add an nsitem containing this relation, so that transformExpr
* called on partition bound expressions is able to report errors
* using a proper context.
*/
nsitem = addRangeTableEntryForRelation(pstate, rel, AccessShareLock,
NULL, false, false);
addNSItemToQuery(pstate, nsitem, false, true, true);
bound = transformPartitionBound(pstate, parent, stmt->partbound);
/*
* Check first that the new partition's bound is valid and does not
* overlap with any of existing partitions of the parent.
*/
check_new_partition_bound(relname, parent, bound, pstate);
/*
* If the default partition exists, its partition constraints will
* change after the addition of this new partition such that it won't
* allow any row that qualifies for this new partition. So, check that
* the existing data in the default partition satisfies the constraint
* as it will exist after adding this partition.
*/
if (OidIsValid(defaultPartOid))
{
check_default_partition_contents(parent, defaultRel, bound);
/* Keep the lock until commit. */
table_close(defaultRel, NoLock);
}
/* Update the pg_class entry. */
StorePartitionBound(rel, parent, bound);
table_close(parent, NoLock);
}
/* Store inheritance information for new rel. */
StoreCatalogInheritance(relationId, inheritOids, stmt->partbound != NULL);
/*
* Process the partitioning specification (if any) and store the partition
* key information into the catalog.
*/
if (partitioned)
{
ParseState *pstate;
int partnatts;
AttrNumber partattrs[PARTITION_MAX_KEYS];
Oid partopclass[PARTITION_MAX_KEYS];
Oid partcollation[PARTITION_MAX_KEYS];
List *partexprs = NIL;
pstate = make_parsestate(NULL);
pstate->p_sourcetext = queryString;
partnatts = list_length(stmt->partspec->partParams);
/* Protect fixed-size arrays here and in executor */
if (partnatts > PARTITION_MAX_KEYS)
ereport(ERROR,
(errcode(ERRCODE_TOO_MANY_COLUMNS),
errmsg("cannot partition using more than %d columns",
PARTITION_MAX_KEYS)));
/*
* We need to transform the raw parsetrees corresponding to partition
* expressions into executable expression trees. Like column defaults
* and CHECK constraints, we could not have done the transformation
* earlier.
*/
stmt->partspec = transformPartitionSpec(rel, stmt->partspec);
ComputePartitionAttrs(pstate, rel, stmt->partspec->partParams,
partattrs, &partexprs, partopclass,
partcollation, stmt->partspec->strategy);
StorePartitionKey(rel, stmt->partspec->strategy, partnatts, partattrs,
partexprs,
partopclass, partcollation);
/* make it all visible */
CommandCounterIncrement();
}
/*
* If we're creating a partition, create now all the indexes, triggers,
* FKs defined in the parent.
*
* We can't do it earlier, because DefineIndex wants to know the partition
* key which we just stored.
*/
if (stmt->partbound)
{
Oid parentId = linitial_oid(inheritOids);
Relation parent;
List *idxlist;
ListCell *cell;
/* Already have strong enough lock on the parent */
parent = table_open(parentId, NoLock);
idxlist = RelationGetIndexList(parent);
/*
* For each index in the parent table, create one in the partition
*/
foreach(cell, idxlist)
{
Relation idxRel = index_open(lfirst_oid(cell), AccessShareLock);
AttrMap *attmap;
IndexStmt *idxstmt;
Oid constraintOid;
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
{
if (idxRel->rd_index->indisunique)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot create foreign partition of partitioned table \"%s\"",
RelationGetRelationName(parent)),
errdetail("Table \"%s\" contains indexes that are unique.",
RelationGetRelationName(parent))));
else
{
index_close(idxRel, AccessShareLock);
continue;
}
}
attmap = build_attrmap_by_name(RelationGetDescr(rel),
RelationGetDescr(parent),
false);
idxstmt =
generateClonedIndexStmt(NULL, idxRel,
attmap, &constraintOid);
DefineIndex(RelationGetRelid(rel),
idxstmt,
InvalidOid,
RelationGetRelid(idxRel),
constraintOid,
-1,
false, false, false, false, false);
index_close(idxRel, AccessShareLock);
}
list_free(idxlist);
/*
* If there are any row-level triggers, clone them to the new
* partition.
*/
if (parent->trigdesc != NULL)
CloneRowTriggersToPartition(parent, rel);
/*
* And foreign keys too. Note that because we're freshly creating the
* table, there is no need to verify these new constraints.
*/
CloneForeignKeyConstraints(NULL, parent, rel);
table_close(parent, NoLock);
}
/*
* Now add any newly specified CHECK constraints to the new relation. Same
* as for defaults above, but these need to come after partitioning is set
* up.
*/
if (stmt->constraints)
AddRelationNewConstraints(rel, NIL, stmt->constraints,
true, true, false, queryString);
/*
* Finally, merge the not-null constraints that are declared directly with
* those that come from parent relations (making sure to count inheritance
* appropriately for each), create them, and set the attnotnull flag on
* columns that don't yet have it.
*/
nncols = AddRelationNotNullConstraints(rel, stmt->nnconstraints,
old_notnulls);
foreach(listptr, nncols)
set_attnotnull(NULL, rel, lfirst_int(listptr), false, NoLock);
ObjectAddressSet(address, RelationRelationId, relationId);
/*
* Clean up. We keep lock on new relation (although it shouldn't be
* visible to anyone else anyway, until commit).
*/
relation_close(rel, NoLock);
return address;
}
/*
* BuildDescForRelation
*
* Given a list of ColumnDef nodes, build a TupleDesc.
*
* Note: tdtypeid will need to be filled in later on.
*/
TupleDesc
BuildDescForRelation(const List *columns)
{
int natts;
AttrNumber attnum;
ListCell *l;
TupleDesc desc;
bool has_not_null;
char *attname;
Oid atttypid;
int32 atttypmod;
Oid attcollation;
int attdim;
/*
* allocate a new tuple descriptor
*/
natts = list_length(columns);
desc = CreateTemplateTupleDesc(natts);
has_not_null = false;
attnum = 0;
foreach(l, columns)
{
ColumnDef *entry = lfirst(l);
AclResult aclresult;
Form_pg_attribute att;
/*
* for each entry in the list, get the name and type information from
* the list and have TupleDescInitEntry fill in the attribute
* information we need.
*/
attnum++;
attname = entry->colname;
typenameTypeIdAndMod(NULL, entry->typeName, &atttypid, &atttypmod);
aclresult = object_aclcheck(TypeRelationId, atttypid, GetUserId(), ACL_USAGE);
if (aclresult != ACLCHECK_OK)
aclcheck_error_type(aclresult, atttypid);
attcollation = GetColumnDefCollation(NULL, entry, atttypid);
attdim = list_length(entry->typeName->arrayBounds);
if (attdim > PG_INT16_MAX)
ereport(ERROR,
errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("too many array dimensions"));
if (entry->typeName->setof)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("column \"%s\" cannot be declared SETOF",
attname)));
TupleDescInitEntry(desc, attnum, attname,
atttypid, atttypmod, attdim);
att = TupleDescAttr(desc, attnum - 1);
/* Override TupleDescInitEntry's settings as requested */
TupleDescInitEntryCollation(desc, attnum, attcollation);
/* Fill in additional stuff not handled by TupleDescInitEntry */
att->attnotnull = entry->is_not_null;
has_not_null |= entry->is_not_null;
att->attislocal = entry->is_local;
att->attinhcount = entry->inhcount;
att->attidentity = entry->identity;
att->attgenerated = entry->generated;
att->attcompression = GetAttributeCompression(att->atttypid, entry->compression);
if (entry->storage)
att->attstorage = entry->storage;
else if (entry->storage_name)
att->attstorage = GetAttributeStorage(att->atttypid, entry->storage_name);
}
if (has_not_null)
{
TupleConstr *constr = (TupleConstr *) palloc0(sizeof(TupleConstr));
constr->has_not_null = true;
constr->has_generated_stored = false;
constr->defval = NULL;
constr->missing = NULL;
constr->num_defval = 0;
constr->check = NULL;
constr->num_check = 0;
desc->constr = constr;
}
else
{
desc->constr = NULL;
}
return desc;
}
/*
* Emit the right error or warning message for a "DROP" command issued on a
* non-existent relation
*/
static void
DropErrorMsgNonExistent(RangeVar *rel, char rightkind, bool missing_ok)
{
const struct dropmsgstrings *rentry;
if (rel->schemaname != NULL &&
!OidIsValid(LookupNamespaceNoError(rel->schemaname)))
{
if (!missing_ok)
{
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_SCHEMA),
errmsg("schema \"%s\" does not exist", rel->schemaname)));
}
else
{
ereport(NOTICE,
(errmsg("schema \"%s\" does not exist, skipping",
rel->schemaname)));
}
return;
}
for (rentry = dropmsgstringarray; rentry->kind != '\0'; rentry++)
{
if (rentry->kind == rightkind)
{
if (!missing_ok)
{
ereport(ERROR,
(errcode(rentry->nonexistent_code),
errmsg(rentry->nonexistent_msg, rel->relname)));
}
else
{
ereport(NOTICE, (errmsg(rentry->skipping_msg, rel->relname)));
break;
}
}
}
Assert(rentry->kind != '\0'); /* Should be impossible */
}
/*
* Emit the right error message for a "DROP" command issued on a
* relation of the wrong type
*/
static void
DropErrorMsgWrongType(const char *relname, char wrongkind, char rightkind)
{
const struct dropmsgstrings *rentry;
const struct dropmsgstrings *wentry;
for (rentry = dropmsgstringarray; rentry->kind != '\0'; rentry++)
if (rentry->kind == rightkind)
break;
Assert(rentry->kind != '\0');
for (wentry = dropmsgstringarray; wentry->kind != '\0'; wentry++)
if (wentry->kind == wrongkind)
break;
/* wrongkind could be something we don't have in our table... */
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg(rentry->nota_msg, relname),
(wentry->kind != '\0') ? errhint("%s", _(wentry->drophint_msg)) : 0));
}
/*
* RemoveRelations
* Implements DROP TABLE, DROP INDEX, DROP SEQUENCE, DROP VIEW,
* DROP MATERIALIZED VIEW, DROP FOREIGN TABLE
*/
void
RemoveRelations(DropStmt *drop)
{
ObjectAddresses *objects;
char relkind;
ListCell *cell;
int flags = 0;
LOCKMODE lockmode = AccessExclusiveLock;
/* DROP CONCURRENTLY uses a weaker lock, and has some restrictions */
if (drop->concurrent)
{
/*
* Note that for temporary relations this lock may get upgraded later
* on, but as no other session can access a temporary relation, this
* is actually fine.
*/
lockmode = ShareUpdateExclusiveLock;
Assert(drop->removeType == OBJECT_INDEX);
if (list_length(drop->objects) != 1)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("DROP INDEX CONCURRENTLY does not support dropping multiple objects")));
if (drop->behavior == DROP_CASCADE)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("DROP INDEX CONCURRENTLY does not support CASCADE")));
}
/*
* First we identify all the relations, then we delete them in a single
* performMultipleDeletions() call. This is to avoid unwanted DROP
* RESTRICT errors if one of the relations depends on another.
*/
/* Determine required relkind */
switch (drop->removeType)
{
case OBJECT_TABLE:
relkind = RELKIND_RELATION;
break;
case OBJECT_INDEX:
relkind = RELKIND_INDEX;
break;
case OBJECT_SEQUENCE:
relkind = RELKIND_SEQUENCE;
break;
case OBJECT_VIEW:
relkind = RELKIND_VIEW;
break;
case OBJECT_MATVIEW:
relkind = RELKIND_MATVIEW;
break;
case OBJECT_FOREIGN_TABLE:
relkind = RELKIND_FOREIGN_TABLE;
break;
default:
elog(ERROR, "unrecognized drop object type: %d",
(int) drop->removeType);
relkind = 0; /* keep compiler quiet */
break;
}
/* Lock and validate each relation; build a list of object addresses */
objects = new_object_addresses();
foreach(cell, drop->objects)
{
RangeVar *rel = makeRangeVarFromNameList((List *) lfirst(cell));
Oid relOid;
ObjectAddress obj;
struct DropRelationCallbackState state;
/*
* These next few steps are a great deal like relation_openrv, but we
* don't bother building a relcache entry since we don't need it.
*
* Check for shared-cache-inval messages before trying to access the
* relation. This is needed to cover the case where the name
* identifies a rel that has been dropped and recreated since the
* start of our transaction: if we don't flush the old syscache entry,
* then we'll latch onto that entry and suffer an error later.
*/
AcceptInvalidationMessages();
/* Look up the appropriate relation using namespace search. */
state.expected_relkind = relkind;
state.heap_lockmode = drop->concurrent ?
ShareUpdateExclusiveLock : AccessExclusiveLock;
/* We must initialize these fields to show that no locks are held: */
state.heapOid = InvalidOid;
state.partParentOid = InvalidOid;
relOid = RangeVarGetRelidExtended(rel, lockmode, RVR_MISSING_OK,
RangeVarCallbackForDropRelation,
(void *) &state);
/* Not there? */
if (!OidIsValid(relOid))
{
DropErrorMsgNonExistent(rel, relkind, drop->missing_ok);
continue;
}
/*
* Decide if concurrent mode needs to be used here or not. The
* callback retrieved the rel's persistence for us.
*/
if (drop->concurrent &&
state.actual_relpersistence != RELPERSISTENCE_TEMP)
{
Assert(list_length(drop->objects) == 1 &&
drop->removeType == OBJECT_INDEX);
flags |= PERFORM_DELETION_CONCURRENTLY;
}
/*
* Concurrent index drop cannot be used with partitioned indexes,
* either.
*/
if ((flags & PERFORM_DELETION_CONCURRENTLY) != 0 &&
state.actual_relkind == RELKIND_PARTITIONED_INDEX)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot drop partitioned index \"%s\" concurrently",
rel->relname)));
/*
* If we're told to drop a partitioned index, we must acquire lock on
* all the children of its parent partitioned table before proceeding.
* Otherwise we'd try to lock the child index partitions before their
* tables, leading to potential deadlock against other sessions that
* will lock those objects in the other order.
*/
if (state.actual_relkind == RELKIND_PARTITIONED_INDEX)
(void) find_all_inheritors(state.heapOid,
state.heap_lockmode,
NULL);
/* OK, we're ready to delete this one */
obj.classId = RelationRelationId;
obj.objectId = relOid;
obj.objectSubId = 0;
add_exact_object_address(&obj, objects);
}
performMultipleDeletions(objects, drop->behavior, flags);
free_object_addresses(objects);
}
/*
* Before acquiring a table lock, check whether we have sufficient rights.
* In the case of DROP INDEX, also try to lock the table before the index.
* Also, if the table to be dropped is a partition, we try to lock the parent
* first.
*/
static void
RangeVarCallbackForDropRelation(const RangeVar *rel, Oid relOid, Oid oldRelOid,
void *arg)
{
HeapTuple tuple;
struct DropRelationCallbackState *state;
char expected_relkind;
bool is_partition;
Form_pg_class classform;
LOCKMODE heap_lockmode;
bool invalid_system_index = false;
state = (struct DropRelationCallbackState *) arg;
heap_lockmode = state->heap_lockmode;
/*
* If we previously locked some other index's heap, and the name we're
* looking up no longer refers to that relation, release the now-useless
* lock.
*/
if (relOid != oldRelOid && OidIsValid(state->heapOid))
{
UnlockRelationOid(state->heapOid, heap_lockmode);
state->heapOid = InvalidOid;
}
/*
* Similarly, if we previously locked some other partition's heap, and the
* name we're looking up no longer refers to that relation, release the
* now-useless lock.
*/
if (relOid != oldRelOid && OidIsValid(state->partParentOid))
{
UnlockRelationOid(state->partParentOid, AccessExclusiveLock);
state->partParentOid = InvalidOid;
}
/* 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);
is_partition = classform->relispartition;
/* Pass back some data to save lookups in RemoveRelations */
state->actual_relkind = classform->relkind;
state->actual_relpersistence = classform->relpersistence;
/*
* Both RELKIND_RELATION and RELKIND_PARTITIONED_TABLE are OBJECT_TABLE,
* but RemoveRelations() can only pass one relkind for a given relation.
* It chooses RELKIND_RELATION for both regular and partitioned tables.
* That means we must be careful before giving the wrong type error when
* the relation is RELKIND_PARTITIONED_TABLE. An equivalent problem
* exists with indexes.
*/
if (classform->relkind == RELKIND_PARTITIONED_TABLE)
expected_relkind = RELKIND_RELATION;
else if (classform->relkind == RELKIND_PARTITIONED_INDEX)
expected_relkind = RELKIND_INDEX;
else
expected_relkind = classform->relkind;
if (state->expected_relkind != expected_relkind)
DropErrorMsgWrongType(rel->relname, classform->relkind,
state->expected_relkind);
/* Allow DROP to either table owner or schema owner */
if (!object_ownercheck(RelationRelationId, relOid, GetUserId()) &&
!object_ownercheck(NamespaceRelationId, classform->relnamespace, GetUserId()))
aclcheck_error(ACLCHECK_NOT_OWNER,
get_relkind_objtype(classform->relkind),
rel->relname);
/*
* Check the case of a system index that might have been invalidated by a
* failed concurrent process and allow its drop. For the time being, this
* only concerns indexes of toast relations that became invalid during a
* REINDEX CONCURRENTLY process.
*/
if (IsSystemClass(relOid, classform) && classform->relkind == RELKIND_INDEX)
{
HeapTuple locTuple;
Form_pg_index indexform;
bool indisvalid;
locTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(relOid));
if (!HeapTupleIsValid(locTuple))
{
ReleaseSysCache(tuple);
return;
}
indexform = (Form_pg_index) GETSTRUCT(locTuple);
indisvalid = indexform->indisvalid;
ReleaseSysCache(locTuple);
/* Mark object as being an invalid index of system catalogs */
if (!indisvalid)
invalid_system_index = true;
}
/* In the case of an invalid index, it is fine to bypass this check */
if (!invalid_system_index && !allowSystemTableMods && IsSystemClass(relOid, classform))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied: \"%s\" is a system catalog",
rel->relname)));
ReleaseSysCache(tuple);
/*
* In DROP INDEX, attempt to acquire lock on the parent table before
* locking the index. index_drop() will need this anyway, and since
* regular queries lock tables before their indexes, we risk deadlock if
* we do it the other way around. No error if we don't find a pg_index
* entry, though --- the relation may have been dropped. Note that this
* code will execute for either plain or partitioned indexes.
*/
if (expected_relkind == RELKIND_INDEX &&
relOid != oldRelOid)
{
state->heapOid = IndexGetRelation(relOid, true);
if (OidIsValid(state->heapOid))
LockRelationOid(state->heapOid, heap_lockmode);
}
/*
* Similarly, if the relation is a partition, we must acquire lock on its
* parent before locking the partition. That's because queries lock the
* parent before its partitions, so we risk deadlock if we do it the other
* way around.
*/
if (is_partition && relOid != oldRelOid)
{
state->partParentOid = get_partition_parent(relOid, true);
if (OidIsValid(state->partParentOid))
LockRelationOid(state->partParentOid, AccessExclusiveLock);
}
}
/*
* ExecuteTruncate
* Executes a TRUNCATE command.
*
* This is a multi-relation truncate. We first open and grab exclusive
* lock on all relations involved, checking permissions and otherwise
* verifying that the relation is OK for truncation. Note that if relations
* are foreign tables, at this stage, we have not yet checked that their
* foreign data in external data sources are OK for truncation. These are
* checked when foreign data are actually truncated later. In CASCADE mode,
* relations having FK references to the targeted relations are automatically
* added to the group; in RESTRICT mode, we check that all FK references are
* internal to the group that's being truncated. Finally all the relations
* are truncated and reindexed.
*/
void
ExecuteTruncate(TruncateStmt *stmt)
{
List *rels = NIL;
List *relids = NIL;
List *relids_logged = NIL;
ListCell *cell;
/*
* Open, exclusive-lock, and check all the explicitly-specified relations
*/
foreach(cell, stmt->relations)
{
RangeVar *rv = lfirst(cell);
Relation rel;
bool recurse = rv->inh;
Oid myrelid;
LOCKMODE lockmode = AccessExclusiveLock;
myrelid = RangeVarGetRelidExtended(rv, lockmode,
0, RangeVarCallbackForTruncate,
NULL);
/* don't throw error for "TRUNCATE foo, foo" */
if (list_member_oid(relids, myrelid))
continue;
/* open the relation, we already hold a lock on it */
rel = table_open(myrelid, NoLock);
/*
* RangeVarGetRelidExtended() has done most checks with its callback,
* but other checks with the now-opened Relation remain.
*/
truncate_check_activity(rel);
rels = lappend(rels, rel);
relids = lappend_oid(relids, myrelid);
/* Log this relation only if needed for logical decoding */
if (RelationIsLogicallyLogged(rel))
relids_logged = lappend_oid(relids_logged, myrelid);
if (recurse)
{
ListCell *child;
List *children;
children = find_all_inheritors(myrelid, lockmode, NULL);
foreach(child, children)
{
Oid childrelid = lfirst_oid(child);
if (list_member_oid(relids, childrelid))
continue;
/* find_all_inheritors already got lock */
rel = table_open(childrelid, NoLock);
/*
* It is possible that the parent table has children that are
* temp tables of other backends. We cannot safely access
* such tables (because of buffering issues), and the best
* thing to do is to silently ignore them. Note that this
* check is the same as one of the checks done in
* truncate_check_activity() called below, still it is kept
* here for simplicity.
*/
if (RELATION_IS_OTHER_TEMP(rel))
{
table_close(rel, lockmode);
continue;
}
/*
* Inherited TRUNCATE commands perform access permission
* checks on the parent table only. So we skip checking the
* children's permissions and don't call
* truncate_check_perms() here.
*/
truncate_check_rel(RelationGetRelid(rel), rel->rd_rel);
truncate_check_activity(rel);
rels = lappend(rels, rel);
relids = lappend_oid(relids, childrelid);
/* Log this relation only if needed for logical decoding */
if (RelationIsLogicallyLogged(rel))
relids_logged = lappend_oid(relids_logged, childrelid);
}
}
else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot truncate only a partitioned table"),
errhint("Do not specify the ONLY keyword, or use TRUNCATE ONLY on the partitions directly.")));
}
ExecuteTruncateGuts(rels, relids, relids_logged,
stmt->behavior, stmt->restart_seqs, false);
/* And close the rels */
foreach(cell, rels)
{
Relation rel = (Relation) lfirst(cell);
table_close(rel, NoLock);
}
}
/*
* ExecuteTruncateGuts
*
* Internal implementation of TRUNCATE. This is called by the actual TRUNCATE
* command (see above) as well as replication subscribers that execute a
* replicated TRUNCATE action.
*
* explicit_rels is the list of Relations to truncate that the command
* specified. relids is the list of Oids corresponding to explicit_rels.
* relids_logged is the list of Oids (a subset of relids) that require
* WAL-logging. This is all a bit redundant, but the existing callers have
* this information handy in this form.
*/
void
ExecuteTruncateGuts(List *explicit_rels,
List *relids,
List *relids_logged,
DropBehavior behavior, bool restart_seqs,
bool run_as_table_owner)
{
List *rels;
List *seq_relids = NIL;
HTAB *ft_htab = NULL;
EState *estate;
ResultRelInfo *resultRelInfos;
ResultRelInfo *resultRelInfo;
SubTransactionId mySubid;
ListCell *cell;
Oid *logrelids;
/*
* Check the explicitly-specified relations.
*
* In CASCADE mode, suck in all referencing relations as well. This
* requires multiple iterations to find indirectly-dependent relations. At
* each phase, we need to exclusive-lock new rels before looking for their
* dependencies, else we might miss something. Also, we check each rel as
* soon as we open it, to avoid a faux pas such as holding lock for a long
* time on a rel we have no permissions for.
*/
rels = list_copy(explicit_rels);
if (behavior == DROP_CASCADE)
{
for (;;)
{
List *newrelids;
newrelids = heap_truncate_find_FKs(relids);
if (newrelids == NIL)
break; /* nothing else to add */
foreach(cell, newrelids)
{
Oid relid = lfirst_oid(cell);
Relation rel;
rel = table_open(relid, AccessExclusiveLock);
ereport(NOTICE,
(errmsg("truncate cascades to table \"%s\"",
RelationGetRelationName(rel))));
truncate_check_rel(relid, rel->rd_rel);
truncate_check_perms(relid, rel->rd_rel);
truncate_check_activity(rel);
rels = lappend(rels, rel);
relids = lappend_oid(relids, relid);
/* Log this relation only if needed for logical decoding */
if (RelationIsLogicallyLogged(rel))
relids_logged = lappend_oid(relids_logged, relid);
}
}
}
/*
* Check foreign key references. In CASCADE mode, this should be
* unnecessary since we just pulled in all the references; but as a
* cross-check, do it anyway if in an Assert-enabled build.
*/
#ifdef USE_ASSERT_CHECKING
heap_truncate_check_FKs(rels, false);
#else
if (behavior == DROP_RESTRICT)
heap_truncate_check_FKs(rels, false);
#endif
/*
* If we are asked to restart sequences, find all the sequences, lock them
* (we need AccessExclusiveLock for ResetSequence), and check permissions.
* We want to do this early since it's pointless to do all the truncation
* work only to fail on sequence permissions.
*/
if (restart_seqs)
{
foreach(cell, rels)
{
Relation rel = (Relation) lfirst(cell);
List *seqlist = getOwnedSequences(RelationGetRelid(rel));
ListCell *seqcell;
foreach(seqcell, seqlist)
{
Oid seq_relid = lfirst_oid(seqcell);
Relation seq_rel;
seq_rel = relation_open(seq_relid, AccessExclusiveLock);
/* This check must match AlterSequence! */
if (!object_ownercheck(RelationRelationId, seq_relid, GetUserId()))
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SEQUENCE,
RelationGetRelationName(seq_rel));
seq_relids = lappend_oid(seq_relids, seq_relid);
relation_close(seq_rel, NoLock);
}
}
}
/* Prepare to catch AFTER triggers. */
AfterTriggerBeginQuery();
/*
* To fire triggers, we'll need an EState as well as a ResultRelInfo for
* each relation. We don't need to call ExecOpenIndices, though.
*
* We put the ResultRelInfos in the es_opened_result_relations list, even
* though we don't have a range table and don't populate the
* es_result_relations array. That's a bit bogus, but it's enough to make
* ExecGetTriggerResultRel() find them.
*/
estate = CreateExecutorState();
resultRelInfos = (ResultRelInfo *)
palloc(list_length(rels) * sizeof(ResultRelInfo));
resultRelInfo = resultRelInfos;
foreach(cell, rels)
{
Relation rel = (Relation) lfirst(cell);
InitResultRelInfo(resultRelInfo,
rel,
0, /* dummy rangetable index */
NULL,
0);
estate->es_opened_result_relations =
lappend(estate->es_opened_result_relations, resultRelInfo);
resultRelInfo++;
}
/*
* Process all BEFORE STATEMENT TRUNCATE triggers before we begin
* truncating (this is because one of them might throw an error). Also, if
* we were to allow them to prevent statement execution, that would need
* to be handled here.
*/
resultRelInfo = resultRelInfos;
foreach(cell, rels)
{
UserContext ucxt;
if (run_as_table_owner)
SwitchToUntrustedUser(resultRelInfo->ri_RelationDesc->rd_rel->relowner,
&ucxt);
ExecBSTruncateTriggers(estate, resultRelInfo);
if (run_as_table_owner)
RestoreUserContext(&ucxt);
resultRelInfo++;
}
/*
* OK, truncate each table.
*/
mySubid = GetCurrentSubTransactionId();
foreach(cell, rels)
{
Relation rel = (Relation) lfirst(cell);
/* Skip partitioned tables as there is nothing to do */
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
continue;
/*
* Build the lists of foreign tables belonging to each foreign server
* and pass each list to the foreign data wrapper's callback function,
* so that each server can truncate its all foreign tables in bulk.
* Each list is saved as a single entry in a hash table that uses the
* server OID as lookup key.
*/
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
{
Oid serverid = GetForeignServerIdByRelId(RelationGetRelid(rel));
bool found;
ForeignTruncateInfo *ft_info;
/* First time through, initialize hashtable for foreign tables */
if (!ft_htab)
{
HASHCTL hctl;
memset(&hctl, 0, sizeof(HASHCTL));
hctl.keysize = sizeof(Oid);
hctl.entrysize = sizeof(ForeignTruncateInfo);
hctl.hcxt = CurrentMemoryContext;
ft_htab = hash_create("TRUNCATE for Foreign Tables",
32, /* start small and extend */
&hctl,
HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
}
/* Find or create cached entry for the foreign table */
ft_info = hash_search(ft_htab, &serverid, HASH_ENTER, &found);
if (!found)
ft_info->rels = NIL;
/*
* Save the foreign table in the entry of the server that the
* foreign table belongs to.
*/
ft_info->rels = lappend(ft_info->rels, rel);
continue;
}
/*
* Normally, we need a transaction-safe truncation here. However, if
* the table was either created in the current (sub)transaction or has
* a new relfilenumber in the current (sub)transaction, then we can
* just truncate it in-place, because a rollback would cause the whole
* table or the current physical file to be thrown away anyway.
*/
if (rel->rd_createSubid == mySubid ||
rel->rd_newRelfilelocatorSubid == mySubid)
{
/* Immediate, non-rollbackable truncation is OK */
heap_truncate_one_rel(rel);
}
else
{
Oid heap_relid;
Oid toast_relid;
ReindexParams reindex_params = {0};
/*
* This effectively deletes all rows in the table, and may be done
* in a serializable transaction. In that case we must record a
* rw-conflict in to this transaction from each transaction
* holding a predicate lock on the table.
*/
CheckTableForSerializableConflictIn(rel);
/*
* Need the full transaction-safe pushups.
*
* Create a new empty storage file for the relation, and assign it
* as the relfilenumber value. The old storage file is scheduled
* for deletion at commit.
*/
RelationSetNewRelfilenumber(rel, rel->rd_rel->relpersistence);
heap_relid = RelationGetRelid(rel);
/*
* The same for the toast table, if any.
*/
toast_relid = rel->rd_rel->reltoastrelid;
if (OidIsValid(toast_relid))
{
Relation toastrel = relation_open(toast_relid,
AccessExclusiveLock);
RelationSetNewRelfilenumber(toastrel,
toastrel->rd_rel->relpersistence);
table_close(toastrel, NoLock);
}
/*
* Reconstruct the indexes to match, and we're done.
*/
reindex_relation(NULL, heap_relid, REINDEX_REL_PROCESS_TOAST,
&reindex_params);
}
pgstat_count_truncate(rel);
}
/* Now go through the hash table, and truncate foreign tables */
if (ft_htab)
{
ForeignTruncateInfo *ft_info;
HASH_SEQ_STATUS seq;
hash_seq_init(&seq, ft_htab);
PG_TRY();
{
while ((ft_info = hash_seq_search(&seq)) != NULL)
{
FdwRoutine *routine = GetFdwRoutineByServerId(ft_info->serverid);
/* truncate_check_rel() has checked that already */
Assert(routine->ExecForeignTruncate != NULL);
routine->ExecForeignTruncate(ft_info->rels,
behavior,
restart_seqs);
}
}
PG_FINALLY();
{
hash_destroy(ft_htab);
}
PG_END_TRY();
}
/*
* Restart owned sequences if we were asked to.
*/
foreach(cell, seq_relids)
{
Oid seq_relid = lfirst_oid(cell);
ResetSequence(seq_relid);
}
/*
* Write a WAL record to allow this set of actions to be logically
* decoded.
*
* Assemble an array of relids so we can write a single WAL record for the
* whole action.
*/
if (relids_logged != NIL)
{
xl_heap_truncate xlrec;
int i = 0;
/* should only get here if wal_level >= logical */
Assert(XLogLogicalInfoActive());
logrelids = palloc(list_length(relids_logged) * sizeof(Oid));
foreach(cell, relids_logged)
logrelids[i++] = lfirst_oid(cell);
xlrec.dbId = MyDatabaseId;
xlrec.nrelids = list_length(relids_logged);
xlrec.flags = 0;
if (behavior == DROP_CASCADE)
xlrec.flags |= XLH_TRUNCATE_CASCADE;
if (restart_seqs)
xlrec.flags |= XLH_TRUNCATE_RESTART_SEQS;
XLogBeginInsert();
XLogRegisterData((char *) &xlrec, SizeOfHeapTruncate);
XLogRegisterData((char *) logrelids, list_length(relids_logged) * sizeof(Oid));
XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN);
(void) XLogInsert(RM_HEAP_ID, XLOG_HEAP_TRUNCATE);
}
/*
* Process all AFTER STATEMENT TRUNCATE triggers.
*/
resultRelInfo = resultRelInfos;
foreach(cell, rels)
{
UserContext ucxt;
if (run_as_table_owner)
SwitchToUntrustedUser(resultRelInfo->ri_RelationDesc->rd_rel->relowner,
&ucxt);
ExecASTruncateTriggers(estate, resultRelInfo);
if (run_as_table_owner)
RestoreUserContext(&ucxt);
resultRelInfo++;
}
/* Handle queued AFTER triggers */
AfterTriggerEndQuery(estate);
/* We can clean up the EState now */
FreeExecutorState(estate);
/*
* Close any rels opened by CASCADE (can't do this while EState still
* holds refs)
*/
rels = list_difference_ptr(rels, explicit_rels);
foreach(cell, rels)
{
Relation rel = (Relation) lfirst(cell);
table_close(rel, NoLock);
}
}
/*
* Check that a given relation is safe to truncate. Subroutine for
* ExecuteTruncate() and RangeVarCallbackForTruncate().
*/
static void
truncate_check_rel(Oid relid, Form_pg_class reltuple)
{
char *relname = NameStr(reltuple->relname);
/*
* Only allow truncate on regular tables, foreign tables using foreign
* data wrappers supporting TRUNCATE and partitioned tables (although, the
* latter are only being included here for the following checks; no
* physical truncation will occur in their case.).
*/
if (reltuple->relkind == RELKIND_FOREIGN_TABLE)
{
Oid serverid = GetForeignServerIdByRelId(relid);
FdwRoutine *fdwroutine = GetFdwRoutineByServerId(serverid);
if (!fdwroutine->ExecForeignTruncate)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot truncate foreign table \"%s\"",
relname)));
}
else if (reltuple->relkind != RELKIND_RELATION &&
reltuple->relkind != RELKIND_PARTITIONED_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not a table", relname)));
/*
* Most system catalogs can't be truncated at all, or at least not unless
* allow_system_table_mods=on. As an exception, however, we allow
* pg_largeobject to be truncated as part of pg_upgrade, because we need
* to change its relfilenode to match the old cluster, and allowing a
* TRUNCATE command to be executed is the easiest way of doing that.
*/
if (!allowSystemTableMods && IsSystemClass(relid, reltuple)
&& (!IsBinaryUpgrade || relid != LargeObjectRelationId))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied: \"%s\" is a system catalog",
relname)));
InvokeObjectTruncateHook(relid);
}
/*
* Check that current user has the permission to truncate given relation.
*/
static void
truncate_check_perms(Oid relid, Form_pg_class reltuple)
{
char *relname = NameStr(reltuple->relname);
AclResult aclresult;
/* Permissions checks */
aclresult = pg_class_aclcheck(relid, GetUserId(), ACL_TRUNCATE);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult, get_relkind_objtype(reltuple->relkind),
relname);
}
/*
* Set of extra sanity checks to check if a given relation is safe to
* truncate. This is split with truncate_check_rel() as
* RangeVarCallbackForTruncate() cannot open a Relation yet.
*/
static void
truncate_check_activity(Relation rel)
{
/*
* Don't allow truncate on temp tables of other backends ... their local
* buffer manager is not going to cope.
*/
if (RELATION_IS_OTHER_TEMP(rel))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot truncate temporary tables of other sessions")));
/*
* Also check for active uses of the relation in the current transaction,
* including open scans and pending AFTER trigger events.
*/
CheckTableNotInUse(rel, "TRUNCATE");
}
/*
* storage_name
* returns the name corresponding to a typstorage/attstorage enum value
*/
static const char *
storage_name(char c)
{
switch (c)
{
case TYPSTORAGE_PLAIN:
return "PLAIN";
case TYPSTORAGE_EXTERNAL:
return "EXTERNAL";
case TYPSTORAGE_EXTENDED:
return "EXTENDED";
case TYPSTORAGE_MAIN:
return "MAIN";
default:
return "???";
}
}
/*----------
* MergeAttributes
* Returns new schema given initial schema and superclasses.
*
* Input arguments:
* 'columns' is the column/attribute definition for the table. (It's a list
* of ColumnDef's.) It is destructively changed.
* 'supers' is a list of OIDs of parent relations, already locked by caller.
* 'relpersistence' is the persistence type of the table.
* 'is_partition' tells if the table is a partition.
*
* Output arguments:
* 'supconstr' receives a list of constraints belonging to the parents,
* updated as necessary to be valid for the child.
* 'supnotnulls' receives a list of CookedConstraints that corresponds to
* constraints coming from inheritance parents.
*
* Return value:
* Completed schema list.
*
* Notes:
* The order in which the attributes are inherited is very important.
* Intuitively, the inherited attributes should come first. If a table
* inherits from multiple parents, the order of those attributes are
* according to the order of the parents specified in CREATE TABLE.
*
* Here's an example:
*
* create table person (name text, age int4, location point);
* create table emp (salary int4, manager text) inherits(person);
* create table student (gpa float8) inherits (person);
* create table stud_emp (percent int4) inherits (emp, student);
*
* The order of the attributes of stud_emp is:
*
* person {1:name, 2:age, 3:location}
* / \
* {6:gpa} student emp {4:salary, 5:manager}
* \ /
* stud_emp {7:percent}
*
* If the same attribute name appears multiple times, then it appears
* in the result table in the proper location for its first appearance.
*
* Constraints (including not-null constraints) for the child table
* are the union of all relevant constraints, from both the child schema
* and parent tables. In addition, in legacy inheritance, each column that
* appears in a primary key in any of the parents also gets a NOT NULL
* constraint (partitioning doesn't need this, because the PK itself gets
* inherited.)
*
* The default value for a child column is defined as:
* (1) If the child schema specifies a default, that value is used.
* (2) If neither the child nor any parent specifies a default, then
* the column will not have a default.
* (3) If conflicting defaults are inherited from different parents
* (and not overridden by the child), an error is raised.
* (4) Otherwise the inherited default is used.
*
* Note that the default-value infrastructure is used for generated
* columns' expressions too, so most of the preceding paragraph applies
* to generation expressions too. We insist that a child column be
* generated if and only if its parent(s) are, but it need not have
* the same generation expression.
*----------
*/
static List *
MergeAttributes(List *columns, const List *supers, char relpersistence,
bool is_partition, List **supconstr, List **supnotnulls)
{
List *inh_columns = NIL;
List *constraints = NIL;
List *nnconstraints = NIL;
bool have_bogus_defaults = false;
int child_attno;
static Node bogus_marker = {0}; /* marks conflicting defaults */
List *saved_columns = NIL;
ListCell *lc;
/*
* Check for and reject tables with too many columns. We perform this
* check relatively early for two reasons: (a) we don't run the risk of
* overflowing an AttrNumber in subsequent code (b) an O(n^2) algorithm is
* okay if we're processing <= 1600 columns, but could take minutes to
* execute if the user attempts to create a table with hundreds of
* thousands of columns.
*
* Note that we also need to check that we do not exceed this figure after
* including columns from inherited relations.
*/
if (list_length(columns) > MaxHeapAttributeNumber)
ereport(ERROR,
(errcode(ERRCODE_TOO_MANY_COLUMNS),
errmsg("tables can have at most %d columns",
MaxHeapAttributeNumber)));
/*
* Check for duplicate names in the explicit list of attributes.
*
* Although we might consider merging such entries in the same way that we
* handle name conflicts for inherited attributes, it seems to make more
* sense to assume such conflicts are errors.
*
* We don't use foreach() here because we have two nested loops over the
* columns list, with possible element deletions in the inner one. If we
* used foreach_delete_current() it could only fix up the state of one of
* the loops, so it seems cleaner to use looping over list indexes for
* both loops. Note that any deletion will happen beyond where the outer
* loop is, so its index never needs adjustment.
*/
for (int coldefpos = 0; coldefpos < list_length(columns); coldefpos++)
{
ColumnDef *coldef = list_nth_node(ColumnDef, columns, coldefpos);
if (!is_partition && coldef->typeName == NULL)
{
/*
* Typed table column option that does not belong to a column from
* the type. This works because the columns from the type come
* first in the list. (We omit this check for partition column
* lists; those are processed separately below.)
*/
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_COLUMN),
errmsg("column \"%s\" does not exist",
coldef->colname)));
}
/* restpos scans all entries beyond coldef; incr is in loop body */
for (int restpos = coldefpos + 1; restpos < list_length(columns);)
{
ColumnDef *restdef = list_nth_node(ColumnDef, columns, restpos);
if (strcmp(coldef->colname, restdef->colname) == 0)
{
if (coldef->is_from_type)
{
/*
* merge the column options into the column from the type
*/
coldef->is_not_null = restdef->is_not_null;
coldef->raw_default = restdef->raw_default;
coldef->cooked_default = restdef->cooked_default;
coldef->constraints = restdef->constraints;
coldef->is_from_type = false;
columns = list_delete_nth_cell(columns, restpos);
}
else
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_COLUMN),
errmsg("column \"%s\" specified more than once",
coldef->colname)));
}
else
restpos++;
}
}
/*
* In case of a partition, there are no new column definitions, only dummy
* ColumnDefs created for column constraints. Set them aside for now and
* process them at the end.
*/
if (is_partition)
{
saved_columns = columns;
columns = NIL;
}
/*
* Scan the parents left-to-right, and merge their attributes to form a
* list of inherited columns (inh_columns).
*/
child_attno = 0;
foreach(lc, supers)
{
Oid parent = lfirst_oid(lc);
Relation relation;
TupleDesc tupleDesc;
TupleConstr *constr;
AttrMap *newattmap;
List *inherited_defaults;
List *cols_with_defaults;
List *nnconstrs;
ListCell *lc1;
ListCell *lc2;
Bitmapset *pkattrs;
Bitmapset *nncols = NULL;
/* caller already got lock */
relation = table_open(parent, NoLock);
/*
* Check for active uses of the parent partitioned table in the
* current transaction, such as being used in some manner by an
* enclosing command.
*/
if (is_partition)
CheckTableNotInUse(relation, "CREATE TABLE .. PARTITION OF");
/*
* We do not allow partitioned tables and partitions to participate in
* regular inheritance.
*/
if (relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE && !is_partition)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot inherit from partitioned table \"%s\"",
RelationGetRelationName(relation))));
if (relation->rd_rel->relispartition && !is_partition)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot inherit from partition \"%s\"",
RelationGetRelationName(relation))));
if (relation->rd_rel->relkind != RELKIND_RELATION &&
relation->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&
relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("inherited relation \"%s\" is not a table or foreign table",
RelationGetRelationName(relation))));
/*
* If the parent is permanent, so must be all of its partitions. Note
* that inheritance allows that case.
*/
if (is_partition &&
relation->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
relpersistence == RELPERSISTENCE_TEMP)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot create a temporary relation as partition of permanent relation \"%s\"",
RelationGetRelationName(relation))));
/* Permanent rels cannot inherit from temporary ones */
if (relpersistence != RELPERSISTENCE_TEMP &&
relation->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg(!is_partition
? "cannot inherit from temporary relation \"%s\""
: "cannot create a permanent relation as partition of temporary relation \"%s\"",
RelationGetRelationName(relation))));
/* If existing rel is temp, it must belong to this session */
if (relation->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
!relation->rd_islocaltemp)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg(!is_partition
? "cannot inherit from temporary relation of another session"
: "cannot create as partition of temporary relation of another session")));
/*
* We should have an UNDER permission flag for this, but for now,
* demand that creator of a child table own the parent.
*/
if (!object_ownercheck(RelationRelationId, RelationGetRelid(relation), GetUserId()))
aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(relation->rd_rel->relkind),
RelationGetRelationName(relation));
tupleDesc = RelationGetDescr(relation);
constr = tupleDesc->constr;
/*
* newattmap->attnums[] will contain the child-table attribute numbers
* for the attributes of this parent table. (They are not the same
* for parents after the first one, nor if we have dropped columns.)
*/
newattmap = make_attrmap(tupleDesc->natts);
/* We can't process inherited defaults until newattmap is complete. */
inherited_defaults = cols_with_defaults = NIL;
/*
* All columns that are part of the parent's primary key need to be
* NOT NULL; if partition just the attnotnull bit, otherwise a full
* constraint (if they don't have one already). Also, we request
* attnotnull on columns that have a not-null constraint that's not
* marked NO INHERIT.
*/
pkattrs = RelationGetIndexAttrBitmap(relation,
INDEX_ATTR_BITMAP_PRIMARY_KEY);
nnconstrs = RelationGetNotNullConstraints(RelationGetRelid(relation), true);
foreach(lc1, nnconstrs)
nncols = bms_add_member(nncols,
((CookedConstraint *) lfirst(lc1))->attnum);
for (AttrNumber parent_attno = 1; parent_attno <= tupleDesc->natts;
parent_attno++)
{
Form_pg_attribute attribute = TupleDescAttr(tupleDesc,
parent_attno - 1);
char *attributeName = NameStr(attribute->attname);
int exist_attno;
ColumnDef *newdef;
ColumnDef *mergeddef;
/*
* Ignore dropped columns in the parent.
*/
if (attribute->attisdropped)
continue; /* leave newattmap->attnums entry as zero */
/*
* Create new column definition
*/
newdef = makeColumnDef(attributeName, attribute->atttypid,
attribute->atttypmod, attribute->attcollation);
newdef->storage = attribute->attstorage;
newdef->generated = attribute->attgenerated;
if (CompressionMethodIsValid(attribute->attcompression))
newdef->compression =
pstrdup(GetCompressionMethodName(attribute->attcompression));
/*
* 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 (is_partition)
newdef->identity = attribute->attidentity;
/*
* Does it match some previously considered column from another
* parent?
*/
exist_attno = findAttrByName(attributeName, inh_columns);
if (exist_attno > 0)
{
/*
* Yes, try to merge the two column definitions.
*/
mergeddef = MergeInheritedAttribute(inh_columns, exist_attno, newdef);
newattmap->attnums[parent_attno - 1] = exist_attno;
/*
* Partitions have only one parent, so conflict should never
* occur.
*/
Assert(!is_partition);
}
else
{
/*
* No, create a new inherited column
*/
newdef->inhcount = 1;
newdef->is_local = false;
inh_columns = lappend(inh_columns, newdef);
newattmap->attnums[parent_attno - 1] = ++child_attno;
mergeddef = newdef;
}
/*
* mark attnotnull if parent has it and it's not NO INHERIT
*/
if (bms_is_member(parent_attno, nncols) ||
bms_is_member(parent_attno - FirstLowInvalidHeapAttributeNumber,
pkattrs))
mergeddef->is_not_null = true;
/*
* In regular inheritance, columns in the parent's primary key get
* an extra not-null constraint. Partitioning doesn't need this,
* because the PK itself is going to be cloned to the partition.
*/
if (!is_partition &&
bms_is_member(parent_attno -
FirstLowInvalidHeapAttributeNumber,
pkattrs))
{
CookedConstraint *nn;
nn = palloc(sizeof(CookedConstraint));
nn->contype = CONSTR_NOTNULL;
nn->conoid = InvalidOid;
nn->name = NULL;
nn->attnum = newattmap->attnums[parent_attno - 1];
nn->expr = NULL;
nn->skip_validation = false;
nn->is_local = false;
nn->inhcount = 1;
nn->is_no_inherit = false;
nnconstraints = lappend(nnconstraints, nn);
}
/*
* Locate default/generation expression if any
*/
if (attribute->atthasdef)
{
Node *this_default;
this_default = TupleDescGetDefault(tupleDesc, parent_attno);
if (this_default == NULL)
elog(ERROR, "default expression not found for attribute %d of relation \"%s\"",
parent_attno, RelationGetRelationName(relation));
/*
* If it's a GENERATED default, it might contain Vars that
* need to be mapped to the inherited column(s)' new numbers.
* We can't do that till newattmap is ready, so just remember
* all the inherited default expressions for the moment.
*/
inherited_defaults = lappend(inherited_defaults, this_default);
cols_with_defaults = lappend(cols_with_defaults, mergeddef);
}
}
/*
* Now process any inherited default expressions, adjusting attnos
* using the completed newattmap map.
*/
forboth(lc1, inherited_defaults, lc2, cols_with_defaults)
{
Node *this_default = (Node *) lfirst(lc1);
ColumnDef *def = (ColumnDef *) lfirst(lc2);
bool found_whole_row;
/* Adjust Vars to match new table's column numbering */
this_default = map_variable_attnos(this_default,
1, 0,
newattmap,
InvalidOid, &found_whole_row);
/*
* For the moment we have to reject whole-row variables. We could
* convert them, if we knew the new table's rowtype OID, but that
* hasn't been assigned yet. (A variable could only appear in a
* generation expression, so the error message is correct.)
*/
if (found_whole_row)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot convert whole-row table reference"),
errdetail("Generation expression for column \"%s\" contains a whole-row reference to table \"%s\".",
def->colname,
RelationGetRelationName(relation))));
/*
* If we already had a default from some prior parent, check to
* see if they are the same. If so, no problem; if not, mark the
* column as having a bogus default. Below, we will complain if
* the bogus default isn't overridden by the child columns.
*/
Assert(def->raw_default == NULL);
if (def->cooked_default == NULL)
def->cooked_default = this_default;
else if (!equal(def->cooked_default, this_default))
{
def->cooked_default = &bogus_marker;
have_bogus_defaults = true;
}
}
/*
* Now copy the CHECK constraints of this parent, adjusting attnos
* using the completed newattmap map. Identically named constraints
* are merged if possible, else we throw error.
*/
if (constr && constr->num_check > 0)
{
ConstrCheck *check = constr->check;
for (int i = 0; i < constr->num_check; i++)
{
char *name = check[i].ccname;
Node *expr;
bool found_whole_row;
/* ignore if the constraint is non-inheritable */
if (check[i].ccnoinherit)
continue;
/* Adjust Vars to match new table's column numbering */
expr = map_variable_attnos(stringToNode(check[i].ccbin),
1, 0,
newattmap,
InvalidOid, &found_whole_row);
/*
* For the moment we have to reject whole-row variables. We
* could convert them, if we knew the new table's rowtype OID,
* but that hasn't been assigned yet.
*/
if (found_whole_row)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot convert whole-row table reference"),
errdetail("Constraint \"%s\" contains a whole-row reference to table \"%s\".",
name,
RelationGetRelationName(relation))));
constraints = MergeCheckConstraint(constraints, name, expr);
}
}
/*
* Also copy the not-null constraints from this parent. The
* attnotnull markings were already installed above.
*/
foreach(lc1, nnconstrs)
{
CookedConstraint *nn = lfirst(lc1);
Assert(nn->contype == CONSTR_NOTNULL);
nn->attnum = newattmap->attnums[nn->attnum - 1];
nn->is_local = false;
nn->inhcount = 1;
nnconstraints = lappend(nnconstraints, nn);
}
free_attrmap(newattmap);
/*
* Close the parent rel, but keep our lock on it until xact commit.
* That will prevent someone else from deleting or ALTERing the parent
* before the child is committed.
*/
table_close(relation, NoLock);
}
/*
* If we had no inherited attributes, the result columns are just the
* explicitly declared columns. Otherwise, we need to merge the declared
* columns into the inherited column list. Although, we never have any
* explicitly declared columns if the table is a partition.
*/
if (inh_columns != NIL)
{
int newcol_attno = 0;
foreach(lc, columns)
{
ColumnDef *newdef = lfirst_node(ColumnDef, lc);
char *attributeName = newdef->colname;
int exist_attno;
/*
* Partitions have only one parent and have no column definitions
* of their own, so conflict should never occur.
*/
Assert(!is_partition);
newcol_attno++;
/*
* Does it match some inherited column?
*/
exist_attno = findAttrByName(attributeName, inh_columns);
if (exist_attno > 0)
{
/*
* Yes, try to merge the two column definitions.
*/
MergeChildAttribute(inh_columns, exist_attno, newcol_attno, newdef);
}
else
{
/*
* No, attach new column unchanged to result columns.
*/
inh_columns = lappend(inh_columns, newdef);
}
}
columns = inh_columns;
/*
* Check that we haven't exceeded the legal # of columns after merging
* in inherited columns.
*/
if (list_length(columns) > MaxHeapAttributeNumber)
ereport(ERROR,
(errcode(ERRCODE_TOO_MANY_COLUMNS),
errmsg("tables can have at most %d columns",
MaxHeapAttributeNumber)));
}
/*
* Now that we have the column definition list for a partition, we can
* check whether the columns referenced in the column constraint specs
* actually exist. Also, merge column defaults.
*/
if (is_partition)
{
foreach(lc, saved_columns)
{
ColumnDef *restdef = lfirst(lc);
bool found = false;
ListCell *l;
foreach(l, columns)
{
ColumnDef *coldef = lfirst(l);
if (strcmp(coldef->colname, restdef->colname) == 0)
{
found = true;
/*
* Check for conflicts related to generated columns.
*
* Same rules as above: generated-ness has to match the
* parent, but the contents of the generation expression
* can be different.
*/
if (coldef->generated)
{
if (restdef->raw_default && !restdef->generated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
errmsg("column \"%s\" inherits from generated column but specifies default",
restdef->colname)));
if (restdef->identity)
ereport(ERROR,
(errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
errmsg("column \"%s\" inherits from generated column but specifies identity",
restdef->colname)));
}
else
{
if (restdef->generated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
errmsg("child column \"%s\" specifies generation expression",
restdef->colname),
errhint("A child table column cannot be generated unless its parent column is.")));
}
/*
* Override the parent's default value for this column
* (coldef->cooked_default) with the partition's local
* definition (restdef->raw_default), if there's one. It
* should be physically impossible to get a cooked default
* in the local definition or a raw default in the
* inherited definition, but make sure they're nulls, for
* future-proofing.
*/
Assert(restdef->cooked_default == NULL);
Assert(coldef->raw_default == NULL);
if (restdef->raw_default)
{
coldef->raw_default = restdef->raw_default;
coldef->cooked_default = NULL;
}
}
}
/* complain for constraints on columns not in parent */
if (!found)
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_COLUMN),
errmsg("column \"%s\" does not exist",
restdef->colname)));
}
}
/*
* If we found any conflicting parent default values, check to make sure
* they were overridden by the child.
*/
if (have_bogus_defaults)
{
foreach(lc, columns)
{
ColumnDef *def = lfirst(lc);
if (def->cooked_default == &bogus_marker)
{
if (def->generated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
errmsg("column \"%s\" inherits conflicting generation expressions",
def->colname),
errhint("To resolve the conflict, specify a generation expression explicitly.")));
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
errmsg("column \"%s\" inherits conflicting default values",
def->colname),
errhint("To resolve the conflict, specify a default explicitly.")));
}
}
}
*supconstr = constraints;
*supnotnulls = nnconstraints;
return columns;
}
/*
* MergeCheckConstraint
* Try to merge an inherited CHECK constraint with previous ones
*
* If we inherit identically-named constraints from multiple parents, we must
* merge them, or throw an error if they don't have identical definitions.
*
* constraints is a list of CookedConstraint structs for previous constraints.
*
* If the new constraint matches an existing one, then the existing
* constraint's inheritance count is updated. If there is a conflict (same
* name but different expression), throw an error. If the constraint neither
* matches nor conflicts with an existing one, a new constraint is appended to
* the list.
*/
static List *
MergeCheckConstraint(List *constraints, const char *name, Node *expr)
{
ListCell *lc;
CookedConstraint *newcon;
foreach(lc, constraints)
{
CookedConstraint *ccon = (CookedConstraint *) lfirst(lc);
Assert(ccon->contype == CONSTR_CHECK);
/* Non-matching names never conflict */
if (strcmp(ccon->name, name) != 0)
continue;
if (equal(expr, ccon->expr))
{
/* OK to merge constraint with existing */
ccon->inhcount++;
if (ccon->inhcount < 0)
ereport(ERROR,
errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("too many inheritance parents"));
return constraints;
}
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
errmsg("check constraint name \"%s\" appears multiple times but with different expressions",
name)));
}
/*
* Constraint couldn't be merged with an existing one and also didn't
* conflict with an existing one, so add it as a new one to the list.
*/
newcon = palloc0_object(CookedConstraint);
newcon->contype = CONSTR_CHECK;
newcon->name = pstrdup(name);
newcon->expr = expr;
newcon->inhcount = 1;
return lappend(constraints, newcon);
}
/*
* MergeChildAttribute
* Merge given child attribute definition into given inherited attribute.
*
* Input arguments:
* 'inh_columns' is the list of inherited ColumnDefs.
* 'exist_attno' is the number of the inherited attribute in inh_columns
* 'newcol_attno' is the attribute number in child table's schema definition
* 'newdef' is the column/attribute definition from the child table.
*
* The ColumnDef in 'inh_columns' list is modified. The child attribute's
* ColumnDef remains unchanged.
*
* Notes:
* - The attribute is merged according to the rules laid out in the prologue
* of MergeAttributes().
* - If matching inherited attribute exists but the child attribute can not be
* merged into it, the function throws respective errors.
* - A partition can not have its own column definitions. Hence this function
* is applicable only to a regular inheritance child.
*/
static void
MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const ColumnDef *newdef)
{
char *attributeName = newdef->colname;
ColumnDef *inhdef;
Oid inhtypeid,
newtypeid;
int32 inhtypmod,
newtypmod;
Oid inhcollid,
newcollid;
if (exist_attno == newcol_attno)
ereport(NOTICE,
(errmsg("merging column \"%s\" with inherited definition",
attributeName)));
else
ereport(NOTICE,
(errmsg("moving and merging column \"%s\" with inherited definition", attributeName),
errdetail("User-specified column moved to the position of the inherited column.")));
inhdef = list_nth_node(ColumnDef, inh_columns, exist_attno - 1);
/*
* Must have the same type and typmod
*/
typenameTypeIdAndMod(NULL, inhdef->typeName, &inhtypeid, &inhtypmod);
typenameTypeIdAndMod(NULL, newdef->typeName, &newtypeid, &newtypmod);
if (inhtypeid != newtypeid || inhtypmod != newtypmod)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("column \"%s\" has a type conflict",
attributeName),
errdetail("%s versus %s",
format_type_with_typemod(inhtypeid, inhtypmod),
format_type_with_typemod(newtypeid, newtypmod))));
/*
* Must have the same collation
*/
inhcollid = GetColumnDefCollation(NULL, inhdef, inhtypeid);
newcollid = GetColumnDefCollation(NULL, newdef, newtypeid);
if (inhcollid != newcollid)
ereport(ERROR,
(errcode(ERRCODE_COLLATION_MISMATCH),
errmsg("column \"%s\" has a collation conflict",
attributeName),
errdetail("\"%s\" versus \"%s\"",
get_collation_name(inhcollid),
get_collation_name(newcollid))));
/*
* Identity is never inherited by a regular inheritance child. Pick
* child's identity definition if there's one.
*/
inhdef->identity = newdef->identity;
/*
* Copy storage parameter
*/
if (inhdef->storage == 0)
inhdef->storage = newdef->storage;
else if (newdef->storage != 0 && inhdef->storage != newdef->storage)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("column \"%s\" has a storage parameter conflict",
attributeName),
errdetail("%s versus %s",
storage_name(inhdef->storage),
storage_name(newdef->storage))));
/*
* Copy compression parameter
*/
if (inhdef->compression == NULL)
inhdef->compression = newdef->compression;
else if (newdef->compression != NULL)
{
if (strcmp(inhdef->compression, newdef->compression) != 0)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("column \"%s\" has a compression method conflict",
attributeName),
errdetail("%s versus %s", inhdef->compression, newdef->compression)));
}
/*
* Merge of not-null constraints = OR 'em together
*/
inhdef->is_not_null |= newdef->is_not_null;
/*
* Check for conflicts related to generated columns.
*
* If the parent column is generated, the child column will be made a
* generated column if it isn't already. If it is a generated column,
* we'll take its generation expression in preference to the parent's. We
* must check that the child column doesn't specify a default value or
* identity, which matches the rules for a single column in
* parse_utilcmd.c.
*
* Conversely, if the parent column is not generated, the child column
* can't be either. (We used to allow that, but it results in being able
* to override the generation expression via UPDATEs through the parent.)
*/
if (inhdef->generated)
{
if (newdef->raw_default && !newdef->generated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
errmsg("column \"%s\" inherits from generated column but specifies default",
inhdef->colname)));
if (newdef->identity)
ereport(ERROR,
(errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
errmsg("column \"%s\" inherits from generated column but specifies identity",
inhdef->colname)));
}
else
{
if (newdef->generated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
errmsg("child column \"%s\" specifies generation expression",
inhdef->colname),
errhint("A child table column cannot be generated unless its parent column is.")));
}
/*
* If new def has a default, override previous default
*/
if (newdef->raw_default != NULL)
{
inhdef->raw_default = newdef->raw_default;
inhdef->cooked_default = newdef->cooked_default;
}
/* Mark the column as locally defined */
inhdef->is_local = true;
}
/*
* MergeInheritedAttribute
* Merge given parent attribute definition into specified attribute
* inherited from the previous parents.
*
* Input arguments:
* 'inh_columns' is the list of previously inherited ColumnDefs.
* 'exist_attno' is the number the existing matching attribute in inh_columns.
* 'newdef' is the new parent column/attribute definition to be merged.
*
* The matching ColumnDef in 'inh_columns' list is modified and returned.
*
* Notes:
* - The attribute is merged according to the rules laid out in the prologue
* of MergeAttributes().
* - If matching inherited attribute exists but the new attribute can not be
* merged into it, the function throws respective errors.
* - A partition inherits from only a single parent. Hence this function is
* applicable only to a regular inheritance.
*/
static ColumnDef *
MergeInheritedAttribute(List *inh_columns,
int exist_attno,
const ColumnDef *newdef)
{
char *attributeName = newdef->colname;
ColumnDef *prevdef;
Oid prevtypeid,
newtypeid;
int32 prevtypmod,
newtypmod;
Oid prevcollid,
newcollid;
ereport(NOTICE,
(errmsg("merging multiple inherited definitions of column \"%s\"",
attributeName)));
prevdef = list_nth_node(ColumnDef, inh_columns, exist_attno - 1);
/*
* Must have the same type and typmod
*/
typenameTypeIdAndMod(NULL, prevdef->typeName, &prevtypeid, &prevtypmod);
typenameTypeIdAndMod(NULL, newdef->typeName, &newtypeid, &newtypmod);
if (prevtypeid != newtypeid || prevtypmod != newtypmod)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("inherited column \"%s\" has a type conflict",
attributeName),
errdetail("%s versus %s",
format_type_with_typemod(prevtypeid, prevtypmod),
format_type_with_typemod(newtypeid, newtypmod))));
/*
* Must have the same collation
*/
prevcollid = GetColumnDefCollation(NULL, prevdef, prevtypeid);
newcollid = GetColumnDefCollation(NULL, newdef, newtypeid);
if (prevcollid != newcollid)
ereport(ERROR,
(errcode(ERRCODE_COLLATION_MISMATCH),
errmsg("inherited column \"%s\" has a collation conflict",
attributeName),
errdetail("\"%s\" versus \"%s\"",
get_collation_name(prevcollid),
get_collation_name(newcollid))));
/*
* Copy/check storage parameter
*/
if (prevdef->storage == 0)
prevdef->storage = newdef->storage;
else if (prevdef->storage != newdef->storage)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("inherited column \"%s\" has a storage parameter conflict",
attributeName),
errdetail("%s versus %s",
storage_name(prevdef->storage),
storage_name(newdef->storage))));
/*
* Copy/check compression parameter
*/
if (prevdef->compression == NULL)
prevdef->compression = newdef->compression;
else if (strcmp(prevdef->compression, newdef->compression) != 0)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("column \"%s\" has a compression method conflict",
attributeName),
errdetail("%s versus %s", prevdef->compression, newdef->compression)));
/*
* Check for GENERATED conflicts
*/
if (prevdef->generated != newdef->generated)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("inherited column \"%s\" has a generation conflict",
attributeName)));
/*
* Default and other constraints are handled by the caller.
*/
prevdef->inhcount++;
if (prevdef->inhcount < 0)
ereport(ERROR,
errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("too many inheritance parents"));
return prevdef;
}
/*
* StoreCatalogInheritance
* Updates the system catalogs with proper inheritance information.
*
* supers is a list of the OIDs of the new relation's direct ancestors.
*/
static void
StoreCatalogInheritance(Oid relationId, List *supers,
bool child_is_partition)
{
Relation relation;
int32 seqNumber;
ListCell *entry;
/*
* sanity checks
*/
Assert(OidIsValid(relationId));
if (supers == NIL)
return;
/*
* Store INHERITS information in pg_inherits using direct ancestors only.
* Also enter dependencies on the direct ancestors, and make sure they are
* marked with relhassubclass = true.
*
* (Once upon a time, both direct and indirect ancestors were found here
* and then entered into pg_ipl. Since that catalog doesn't exist
* anymore, there's no need to look for indirect ancestors.)
*/
relation = table_open(InheritsRelationId, RowExclusiveLock);
seqNumber = 1;
foreach(entry, supers)
{
Oid parentOid = lfirst_oid(entry);
StoreCatalogInheritance1(relationId, parentOid, seqNumber, relation,
child_is_partition);
seqNumber++;
}
table_close(relation, RowExclusiveLock);
}
/*
* Make catalog entries showing relationId as being an inheritance child
* of parentOid. inhRelation is the already-opened pg_inherits catalog.
*/
static void
StoreCatalogInheritance1(Oid relationId, Oid parentOid,
int32 seqNumber, Relation inhRelation,
bool child_is_partition)
{
ObjectAddress childobject,
parentobject;
/* store the pg_inherits row */
StoreSingleInheritance(relationId, parentOid, seqNumber);
/*
* Store a dependency too
*/
parentobject.classId = RelationRelationId;
parentobject.objectId = parentOid;
parentobject.objectSubId = 0;
childobject.classId = RelationRelationId;
childobject.objectId = relationId;
childobject.objectSubId = 0;
recordDependencyOn(&childobject, &parentobject,
child_dependency_type(child_is_partition));
/*
* Post creation hook of this inheritance. Since object_access_hook
* doesn't take multiple object identifiers, we relay oid of parent
* relation using auxiliary_id argument.
*/
InvokeObjectPostAlterHookArg(InheritsRelationId,
relationId, 0,
parentOid, false);
/*
* Mark the parent as having subclasses.
*/
SetRelationHasSubclass(parentOid, true);
}
/*
* Look for an existing column entry with the given name.
*
* Returns the index (starting with 1) if attribute already exists in columns,
* 0 if it doesn't.
*/
static int
findAttrByName(const char *attributeName, const List *columns)
{
ListCell *lc;
int i = 1;
foreach(lc, columns)
{
if (strcmp(attributeName, lfirst_node(ColumnDef, lc)->colname) == 0)
return i;
i++;
}
return 0;
}
/*
* SetRelationHasSubclass
* Set the value of the relation's relhassubclass field in pg_class.
*
* NOTE: caller must be holding an appropriate lock on the relation.
* ShareUpdateExclusiveLock is sufficient.
*
* NOTE: an important side-effect of this operation is that an SI invalidation
* message is sent out to all backends --- including me --- causing plans
* referencing the relation to be rebuilt with the new list of children.
* This must happen even if we find that no change is needed in the pg_class
* row.
*/
void
SetRelationHasSubclass(Oid relationId, bool relhassubclass)
{
Relation relationRelation;
HeapTuple tuple;
Form_pg_class classtuple;
/*
* Fetch a modifiable copy of the tuple, modify it, update pg_class.
*/
relationRelation = table_open(RelationRelationId, RowExclusiveLock);
tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relationId));
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for relation %u", relationId);
classtuple = (Form_pg_class) GETSTRUCT(tuple);
if (classtuple->relhassubclass != relhassubclass)
{
classtuple->relhassubclass = relhassubclass;
CatalogTupleUpdate(relationRelation, &tuple->t_self, tuple);
}
else
{
/* no need to change tuple, but force relcache rebuild anyway */
CacheInvalidateRelcacheByTuple(tuple);
}
heap_freetuple(tuple);
table_close(relationRelation, RowExclusiveLock);
}
/*
* CheckRelationTableSpaceMove
* Check if relation can be moved to new tablespace.
*
* NOTE: The caller must hold AccessExclusiveLock on the relation.
*
* Returns true if the relation can be moved to the new tablespace; raises
* an error if it is not possible to do the move; returns false if the move
* would have no effect.
*/
bool
CheckRelationTableSpaceMove(Relation rel, Oid newTableSpaceId)
{
Oid oldTableSpaceId;
/*
* No work if no change in tablespace. Note that MyDatabaseTableSpace is
* stored as 0.
*/
oldTableSpaceId = rel->rd_rel->reltablespace;
if (newTableSpaceId == oldTableSpaceId ||
(newTableSpaceId == MyDatabaseTableSpace && oldTableSpaceId == 0))
return false;
/*
* We cannot support moving mapped relations into different tablespaces.
* (In particular this eliminates all shared catalogs.)
*/
if (RelationIsMapped(rel))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot move system relation \"%s\"",
RelationGetRelationName(rel))));
/* Cannot move a non-shared relation into pg_global */
if (newTableSpaceId == GLOBALTABLESPACE_OID)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("only shared relations can be placed in pg_global tablespace")));
/*
* Do not allow moving temp tables of other backends ... their local
* buffer manager is not going to cope.
*/
if (RELATION_IS_OTHER_TEMP(rel))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot move temporary tables of other sessions")));
return true;
}
/*
* SetRelationTableSpace
* Set new reltablespace and relfilenumber in pg_class entry.
*
* newTableSpaceId is the new tablespace for the relation, and
* newRelFilenumber its new filenumber. If newRelFilenumber is
* InvalidRelFileNumber, this field is not updated.
*
* NOTE: The caller must hold AccessExclusiveLock on the relation.
*
* The caller of this routine had better check if a relation can be
* moved to this new tablespace by calling CheckRelationTableSpaceMove()
* first, and is responsible for making the change visible with
* CommandCounterIncrement().
*/
void
SetRelationTableSpace(Relation rel,
Oid newTableSpaceId,
RelFileNumber newRelFilenumber)
{
Relation pg_class;
HeapTuple tuple;
Form_pg_class rd_rel;
Oid reloid = RelationGetRelid(rel);
Assert(CheckRelationTableSpaceMove(rel, newTableSpaceId));
/* 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. */
rd_rel->reltablespace = (newTableSpaceId == MyDatabaseTableSpace) ?
InvalidOid : newTableSpaceId;
if (RelFileNumberIsValid(newRelFilenumber))
rd_rel->relfilenode = newRelFilenumber;
CatalogTupleUpdate(pg_class, &tuple->t_self, tuple);
/*
* Record dependency on tablespace. This is only required for relations
* that have no physical storage.
*/
if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind))
changeDependencyOnTablespace(RelationRelationId, reloid,
rd_rel->reltablespace);
heap_freetuple(tuple);
table_close(pg_class, RowExclusiveLock);
}
/*
* renameatt_check - basic sanity checks before attribute rename
*/
static void
renameatt_check(Oid myrelid, Form_pg_class classform, bool recursing)
{
char relkind = classform->relkind;
if (classform->reloftype && !recursing)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot rename column of typed table")));
/*
* Renaming the columns of sequences or toast tables doesn't actually
* break anything from the system's point of view, since internal
* references are by attnum. But it doesn't seem right to allow users to
* change names that are hardcoded into the system, hence the following
* restriction.
*/
if (relkind != RELKIND_RELATION &&
relkind != RELKIND_VIEW &&
relkind != RELKIND_MATVIEW &&
relkind != RELKIND_COMPOSITE_TYPE &&
relkind != RELKIND_INDEX &&
relkind != RELKIND_PARTITIONED_INDEX &&
relkind != RELKIND_FOREIGN_TABLE &&
relkind != RELKIND_PARTITIONED_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot rename columns of relation \"%s\"",
NameStr(classform->relname)),
errdetail_relkind_not_supported(relkind)));
/*
* permissions checking. only the owner of a class can change its schema.
*/
if (!object_ownercheck(RelationRelationId, myrelid, GetUserId()))
aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(myrelid)),
NameStr(classform->relname));
if (!allowSystemTableMods && IsSystemClass(myrelid, classform))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied: \"%s\" is a system catalog",
NameStr(classform->relname))));
}
/*
* renameatt_internal - workhorse for renameatt
*
* Return value is the attribute number in the 'myrelid' relation.
*/
static AttrNumber
renameatt_internal(Oid myrelid,
const char *oldattname,
const char *newattname,
bool recurse,
bool recursing,
int expected_parents,
DropBehavior behavior)
{
Relation targetrelation;
Relation attrelation;
HeapTuple atttup;
Form_pg_attribute attform;
AttrNumber attnum;
/*
* Grab an exclusive lock on the target table, which we will NOT release
* until end of transaction.
*/
targetrelation = relation_open(myrelid, AccessExclusiveLock);
renameatt_check(myrelid, RelationGetForm(targetrelation), recursing);
/*
* if the 'recurse' flag is set then we are supposed to rename this
* attribute in all classes that inherit from 'relname' (as well as in
* 'relname').
*
* any permissions or problems with duplicate attributes will cause the
* whole transaction to abort, which is what we want -- all or nothing.
*/
if (recurse)
{
List *child_oids,
*child_numparents;
ListCell *lo,
*li;
/*
* we need the number of parents for each child so that the recursive
* calls to renameatt() can determine whether there are any parents
* outside the inheritance hierarchy being processed.
*/
child_oids = find_all_inheritors(myrelid, AccessExclusiveLock,
&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);
if (childrelid == myrelid)
continue;
/* note we need not recurse again */
renameatt_internal(childrelid, oldattname, newattname, false, true, numparents, behavior);
}
}
else
{
/*
* If we are told not to recurse, there had better not be any child
* tables; else the rename would put them out of step.
*
* expected_parents will only be 0 if we are not already recursing.
*/
if (expected_parents == 0 &&
find_inheritance_children(myrelid, NoLock) != NIL)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("inherited column \"%s\" must be renamed in child tables too",
oldattname)));
}
/* rename attributes in typed tables of composite type */
if (targetrelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE)
{
List *child_oids;
ListCell *lo;
child_oids = find_typed_table_dependencies(targetrelation->rd_rel->reltype,
RelationGetRelationName(targetrelation),
behavior);
foreach(lo, child_oids)
renameatt_internal(lfirst_oid(lo), oldattname, newattname, true, true, 0, behavior);
}
attrelation = table_open(AttributeRelationId, RowExclusiveLock);
atttup = SearchSysCacheCopyAttName(myrelid, oldattname);
if (!HeapTupleIsValid(atttup))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_COLUMN),
errmsg("column \"%s\" does not exist",
oldattname)));
attform = (Form_pg_attribute) GETSTRUCT(atttup);
attnum = attform->attnum;
if (attnum <= 0)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot rename system column \"%s\"",
oldattname)));
/*
* if the attribute is inherited, forbid the renaming. if this is a
* top-level call to renameatt(), then expected_parents will be 0, so the
* effect of this code will be to prohibit the renaming if the attribute
* is inherited at all. if this is a recursive call to renameatt(),
* expected_parents will be the number of parents the current relation has
* within the inheritance hierarchy being processed, so we'll prohibit the
* renaming only if there are additional parents from elsewhere.
*/
if (attform->attinhcount > expected_parents)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("cannot rename inherited column \"%s\"",
oldattname)));
/* new name should not already exist */
(void) check_for_column_name_collision(targetrelation, newattname, false);
/* apply the update */
namestrcpy(&(attform->attname), newattname);
CatalogTupleUpdate(attrelation, &atttup->t_self, atttup);
InvokeObjectPostAlterHook(RelationRelationId, myrelid, attnum);
heap_freetuple(atttup);
table_close(attrelation, RowExclusiveLock);
relation_close(targetrelation, NoLock); /* close rel but keep lock */
return attnum;
}
/*
* Perform permissions and integrity checks before acquiring a relation lock.
*/
static void
RangeVarCallbackForRenameAttribute(const RangeVar *rv, Oid relid, Oid oldrelid,
void *arg)
{
HeapTuple tuple;
Form_pg_class form;
tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
if (!HeapTupleIsValid(tuple))
return; /* concurrently dropped */
form = (Form_pg_class) GETSTRUCT(tuple);
renameatt_check(relid, form, false);
ReleaseSysCache(tuple);
}
/*
* renameatt - changes the name of an attribute in a relation
*
* The returned ObjectAddress is that of the renamed column.
*/
ObjectAddress
renameatt(RenameStmt *stmt)
{
Oid relid;
AttrNumber attnum;
ObjectAddress address;
/* lock level taken here should match renameatt_internal */
relid = RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock,
stmt->missing_ok ? RVR_MISSING_OK : 0,
RangeVarCallbackForRenameAttribute,
NULL);
if (!OidIsValid(relid))
{
ereport(NOTICE,
(errmsg("relation \"%s\" does not exist, skipping",
stmt->relation->relname)));
return InvalidObjectAddress;
}
attnum =
renameatt_internal(relid,
stmt->subname, /* old att name */
stmt->newname, /* new att name */
stmt->relation->inh, /* recursive? */
false, /* recursing? */
0, /* expected inhcount */
stmt->behavior);
ObjectAddressSubSet(address, RelationRelationId, relid, attnum);
return address;
}
/*
* same logic as renameatt_internal
*/
static ObjectAddress
rename_constraint_internal(Oid myrelid,
Oid mytypid,
const char *oldconname,
const char *newconname,
bool recurse,
bool recursing,
int expected_parents)
{
Relation targetrelation = NULL;
Oid constraintOid;
HeapTuple tuple;
Form_pg_constraint con;
ObjectAddress address;
Assert(!myrelid || !mytypid);
if (mytypid)
{
constraintOid = get_domain_constraint_oid(mytypid, oldconname, false);
}
else
{
targetrelation = relation_open(myrelid, AccessExclusiveLock);
/*
* don't tell it whether we're recursing; we allow changing typed
* tables here
*/
renameatt_check(myrelid, RelationGetForm(targetrelation), false);
constraintOid = get_relation_constraint_oid(myrelid, oldconname, false);
}
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constraintOid));
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for constraint %u",
constraintOid);
con = (Form_pg_constraint) GETSTRUCT(tuple);
if (myrelid &&
(con->contype == CONSTRAINT_CHECK ||
con->contype == CONSTRAINT_NOTNULL) &&
!con->connoinherit)
{
if (recurse)
{
List *child_oids,
*child_numparents;
ListCell *lo,
*li;
child_oids = find_all_inheritors(myrelid, AccessExclusiveLock,
&child_numparents);
forboth(lo, child_oids, li, child_numparents)
{
Oid childrelid = lfirst_oid(lo);
int numparents = lfirst_int(li);
if (childrelid == myrelid)
continue;
rename_constraint_internal(childrelid, InvalidOid, oldconname, newconname, false, true, numparents);
}
}
else
{
if (expected_parents == 0 &&
find_inheritance_children(myrelid, NoLock) != NIL)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("inherited constraint \"%s\" must be renamed in child tables too",
oldconname)));
}
if (con->coninhcount > expected_parents)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("cannot rename inherited constraint \"%s\"",
oldconname)));
}
if (con->conindid
&& (con->contype == CONSTRAINT_PRIMARY
|| con->contype == CONSTRAINT_UNIQUE
|| con->contype == CONSTRAINT_EXCLUSION))
/* rename the index; this renames the constraint as well */
RenameRelationInternal(con->conindid, newconname, false, true);
else
RenameConstraintById(constraintOid, newconname);
ObjectAddressSet(address, ConstraintRelationId, constraintOid);
ReleaseSysCache(tuple);
if (targetrelation)
{
/*
* Invalidate relcache so as others can see the new constraint name.
*/
CacheInvalidateRelcache(targetrelation);
relation_close(targetrelation, NoLock); /* close rel but keep lock */
}
return address;
}
ObjectAddress
RenameConstraint(RenameStmt *stmt)
{
Oid relid = InvalidOid;
Oid typid = InvalidOid;
if (stmt->renameType == OBJECT_DOMCONSTRAINT)
{
Relation rel;
HeapTuple tup;
typid = typenameTypeId(NULL, makeTypeNameFromNameList(castNode(List, stmt->object)));
rel = table_open(TypeRelationId, RowExclusiveLock);
tup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
if (!HeapTupleIsValid(tup))
elog(ERROR, "cache lookup failed for type %u", typid);
checkDomainOwner(tup);
ReleaseSysCache(tup);
table_close(rel, NoLock);
}
else
{
/* lock level taken here should match rename_constraint_internal */
relid = RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock,
stmt->missing_ok ? RVR_MISSING_OK : 0,
RangeVarCallbackForRenameAttribute,
NULL);
if (!OidIsValid(relid))
{
ereport(NOTICE,
(errmsg("relation \"%s\" does not exist, skipping",
stmt->relation->relname)));
return InvalidObjectAddress;
}
}
return
rename_constraint_internal(relid, typid,
stmt->subname,
stmt->newname,
(stmt->relation &&
stmt->relation->inh), /* recursive? */
false, /* recursing? */
0 /* expected inhcount */ );
}
/*
* Execute ALTER TABLE/INDEX/SEQUENCE/VIEW/MATERIALIZED VIEW/FOREIGN TABLE
* RENAME
*/
ObjectAddress
RenameRelation(RenameStmt *stmt)
{
bool is_index_stmt = stmt->renameType == OBJECT_INDEX;
Oid relid;
ObjectAddress address;
/*
* Grab an exclusive lock on the target table, index, sequence, view,
* materialized view, or foreign table, which we will NOT release until
* end of transaction.
*
* Lock level used here should match RenameRelationInternal, to avoid lock
* escalation. However, because ALTER INDEX can be used with any relation
* type, we mustn't believe without verification.
*/
for (;;)
{
LOCKMODE lockmode;
char relkind;
bool obj_is_index;
lockmode = is_index_stmt ? ShareUpdateExclusiveLock : AccessExclusiveLock;
relid = RangeVarGetRelidExtended(stmt->relation, lockmode,
stmt->missing_ok ? RVR_MISSING_OK : 0,
RangeVarCallbackForAlterRelation,
(void *) stmt);
if (!OidIsValid(relid))
{
ereport(NOTICE,
(errmsg("relation \"%s\" does not exist, skipping",
stmt->relation->relname)));
return InvalidObjectAddress;
}
/*
* We allow mismatched statement and object types (e.g., ALTER INDEX
* to rename a table), but we might've used the wrong lock level. If
* that happens, retry with the correct lock level. We don't bother
* if we already acquired AccessExclusiveLock with an index, however.
*/
relkind = get_rel_relkind(relid);
obj_is_index = (relkind == RELKIND_INDEX ||
relkind == RELKIND_PARTITIONED_INDEX);
if (obj_is_index || is_index_stmt == obj_is_index)
break;
UnlockRelationOid(relid, lockmode);
is_index_stmt = obj_is_index;
}
/* Do the work */
RenameRelationInternal(relid, stmt->newname, false, is_index_stmt);
ObjectAddressSet(address, RelationRelationId, relid);
return address;
}
/*
* RenameRelationInternal - change the name of a relation
*/
void
RenameRelationInternal(Oid myrelid, const char *newrelname, bool is_internal, bool is_index)
{
Relation targetrelation;
Relation relrelation; /* for RELATION relation */
HeapTuple reltup;
Form_pg_class relform;
Oid namespaceId;
/*
* Grab a lock on the target relation, which we will NOT release until end
* of transaction. We need at least a self-exclusive lock so that
* concurrent DDL doesn't overwrite the rename if they start updating
* while still seeing the old version. The lock also guards against
* triggering relcache reloads in concurrent sessions, which might not
* handle this information changing under them. For indexes, we can use a
* reduced lock level because RelationReloadIndexInfo() handles indexes
* specially.
*/
targetrelation = relation_open(myrelid, is_index ? ShareUpdateExclusiveLock : AccessExclusiveLock);
namespaceId = RelationGetNamespace(targetrelation);
/*
* Find relation's pg_class tuple, and make sure newrelname isn't in use.
*/
relrelation = table_open(RelationRelationId, RowExclusiveLock);
reltup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(myrelid));
if (!HeapTupleIsValid(reltup)) /* shouldn't happen */
elog(ERROR, "cache lookup failed for relation %u", myrelid);
relform = (Form_pg_class) GETSTRUCT(reltup);
if (get_relname_relid(newrelname, namespaceId) != InvalidOid)
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_TABLE),
errmsg("relation \"%s\" already exists",
newrelname)));
/*
* RenameRelation is careful not to believe the caller's idea of the
* relation kind being handled. We don't have to worry about this, but
* let's not be totally oblivious to it. We can process an index as
* not-an-index, but not the other way around.
*/
Assert(!is_index ||
is_index == (targetrelation->rd_rel->relkind == RELKIND_INDEX ||
targetrelation->rd_rel->relkind == RELKIND_PARTITIONED_INDEX));
/*
* Update pg_class tuple with new relname. (Scribbling on reltup is OK
* because it's a copy...)
*/
namestrcpy(&(relform->relname), newrelname);
CatalogTupleUpdate(relrelation, &reltup->t_self, reltup);
InvokeObjectPostAlterHookArg(RelationRelationId, myrelid, 0,
InvalidOid, is_internal);
heap_freetuple(reltup);
table_close(relrelation, RowExclusiveLock);
/*
* Also rename the associated type, if any.
*/
if (OidIsValid(targetrelation->rd_rel->reltype))
RenameTypeInternal(targetrelation->rd_rel->reltype,
newrelname, namespaceId);
/*
* Also rename the associated constraint, if any.
*/
if (targetrelation->rd_rel->relkind == RELKIND_INDEX ||
targetrelation->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)
{
Oid constraintId = get_index_constraint(myrelid);
if (OidIsValid(constraintId))
RenameConstraintById(constraintId, newrelname);
}
/*
* Close rel, but keep lock!
*/
relation_close(targetrelation, NoLock);
}
/*
* ResetRelRewrite - reset relrewrite
*/
void
ResetRelRewrite(Oid myrelid)
{
Relation relrelation; /* for RELATION relation */
HeapTuple reltup;
Form_pg_class relform;
/*
* Find relation's pg_class tuple.
*/
relrelation = table_open(RelationRelationId, RowExclusiveLock);
reltup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(myrelid));
if (!HeapTupleIsValid(reltup)) /* shouldn't happen */
elog(ERROR, "cache lookup failed for relation %u", myrelid);
relform = (Form_pg_class) GETSTRUCT(reltup);
/*
* Update pg_class tuple.
*/
relform->relrewrite = InvalidOid;
CatalogTupleUpdate(relrelation, &reltup->t_self, reltup);
heap_freetuple(reltup);
table_close(relrelation, RowExclusiveLock);
}
/*
* Disallow ALTER TABLE (and similar commands) when the current backend has
* any open reference to the target table besides the one just acquired by
* the calling command; this implies there's an open cursor or active plan.
* We need this check because our lock doesn't protect us against stomping
* on our own foot, only other people's feet!
*
* For ALTER TABLE, the only case known to cause serious trouble is ALTER
* COLUMN TYPE, and some changes are obviously pretty benign, so this could
* possibly be relaxed to only error out for certain types of alterations.
* But the use-case for allowing any of these things is not obvious, so we
* won't work hard at it for now.
*
* We also reject these commands if there are any pending AFTER trigger events
* for the rel. This is certainly necessary for the rewriting variants of
* ALTER TABLE, because they don't preserve tuple TIDs and so the pending
* events would try to fetch the wrong tuples. It might be overly cautious
* in other cases, but again it seems better to err on the side of paranoia.
*
* REINDEX calls this with "rel" referencing the index to be rebuilt; here
* we are worried about active indexscans on the index. The trigger-event
* check can be skipped, since we are doing no damage to the parent table.
*
* The statement name (eg, "ALTER TABLE") is passed for use in error messages.
*/
void
CheckTableNotInUse(Relation rel, const char *stmt)
{
int expected_refcnt;
expected_refcnt = rel->rd_isnailed ? 2 : 1;
if (rel->rd_refcnt != expected_refcnt)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_IN_USE),
/* translator: first %s is a SQL command, eg ALTER TABLE */
errmsg("cannot %s \"%s\" because it is being used by active queries in this session",
stmt, RelationGetRelationName(rel))));
if (rel->rd_rel->relkind != RELKIND_INDEX &&
rel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX &&
AfterTriggerPendingOnRel(RelationGetRelid(rel)))
ereport(ERROR,
(errcode(ERRCODE_OBJECT_IN_USE),
/* translator: first %s is a SQL command, eg ALTER TABLE */
errmsg("cannot %s \"%s\" because it has pending trigger events",
stmt, RelationGetRelationName(rel))));
}
/*
* AlterTableLookupRelation
* Look up, and lock, the OID for the relation named by an alter table
* statement.
*/
Oid
AlterTableLookupRelation(AlterTableStmt *stmt, LOCKMODE lockmode)
{
return RangeVarGetRelidExtended(stmt->relation, lockmode,
stmt->missing_ok ? RVR_MISSING_OK : 0,
RangeVarCallbackForAlterRelation,
(void *) stmt);
}
/*
* AlterTable
* Execute ALTER TABLE, which can be a list of subcommands
*
* ALTER TABLE is performed in three phases:
* 1. Examine subcommands and perform pre-transformation checking.
* 2. Validate and transform subcommands, and update system catalogs.
* 3. Scan table(s) to check new constraints, and optionally recopy
* the data into new table(s).
* Phase 3 is not performed unless one or more of the subcommands requires
* it. The intention of this design is to allow multiple independent
* updates of the table schema to be performed with only one pass over the
* data.
*
* ATPrepCmd performs phase 1. A "work queue" entry is created for
* each table to be affected (there may be multiple affected tables if the
* commands traverse a table inheritance hierarchy). Also we do preliminary
* validation of the subcommands. Because earlier subcommands may change
* the catalog state seen by later commands, there are limits to what can
* be done in this phase. Generally, this phase acquires table locks,
* checks permissions and relkind, and recurses to find child tables.
*
* ATRewriteCatalogs performs phase 2 for each affected table.
* Certain subcommands need to be performed before others to avoid
* unnecessary conflicts; for example, DROP COLUMN should come before
* ADD COLUMN. Therefore phase 1 divides the subcommands into multiple
* lists, one for each logical "pass" of phase 2.
*
* ATRewriteTables performs phase 3 for those tables that need it.
*
* For most subcommand types, phases 2 and 3 do no explicit recursion,
* since phase 1 already does it. However, for certain subcommand types
* it is only possible to determine how to recurse at phase 2 time; for
* those cases, phase 1 sets the cmd->recurse flag.
*
* Thanks to the magic of MVCC, an error anywhere along the way rolls back
* the whole operation; we don't have to do anything special to clean up.
*
* The caller must lock the relation, with an appropriate lock level
* for the subcommands requested, using AlterTableGetLockLevel(stmt->cmds)
* or higher. We pass the lock level down
* so that we can apply it recursively to inherited tables. Note that the
* lock level we want as we recurse might well be higher than required for
* that specific subcommand. So we pass down the overall lock requirement,
* rather than reassess it at lower levels.
*
* The caller also provides a "context" which is to be passed back to
* utility.c when we need to execute a subcommand such as CREATE INDEX.
* Some of the fields therein, such as the relid, are used here as well.
*/
void
AlterTable(AlterTableStmt *stmt, LOCKMODE lockmode,
AlterTableUtilityContext *context)
{
Relation rel;
/* Caller is required to provide an adequate lock. */
rel = relation_open(context->relid, NoLock);
CheckTableNotInUse(rel, "ALTER TABLE");
ATController(stmt, rel, stmt->cmds, stmt->relation->inh, lockmode, context);
}
/*
* AlterTableInternal
*
* ALTER TABLE with target specified by OID
*
* We do not reject if the relation is already open, because it's quite
* likely that one or more layers of caller have it open. That means it
* is unsafe to use this entry point for alterations that could break
* existing query plans. On the assumption it's not used for such, we
* don't have to reject pending AFTER triggers, either.
*
* Also, since we don't have an AlterTableUtilityContext, this cannot be
* used for any subcommand types that require parse transformation or
* could generate subcommands that have to be passed to ProcessUtility.
*/
void
AlterTableInternal(Oid relid, List *cmds, bool recurse)
{
Relation rel;
LOCKMODE lockmode = AlterTableGetLockLevel(cmds);
rel = relation_open(relid, lockmode);
EventTriggerAlterTableRelid(relid);
ATController(NULL, rel, cmds, recurse, lockmode, NULL);
}
/*
* AlterTableGetLockLevel
*
* Sets the overall lock level required for the supplied list of subcommands.
* Policy for doing this set according to needs of AlterTable(), see
* comments there for overall explanation.
*
* Function is called before and after parsing, so it must give same
* answer each time it is called. Some subcommands are transformed
* into other subcommand types, so the transform must never be made to a
* lower lock level than previously assigned. All transforms are noted below.
*
* Since this is called before we lock the table we cannot use table metadata
* to influence the type of lock we acquire.
*
* There should be no lockmodes hardcoded into the subcommand functions. All
* lockmode decisions for ALTER TABLE are made here only. The one exception is
* ALTER TABLE RENAME which is treated as a different statement type T_RenameStmt
* and does not travel through this section of code and cannot be combined with
* any of the subcommands given here.
*
* Note that Hot Standby only knows about AccessExclusiveLocks on the primary
* so any changes that might affect SELECTs running on standbys need to use
* AccessExclusiveLocks even if you think a lesser lock would do, unless you
* have a solution for that also.
*
* Also note that pg_dump uses only an AccessShareLock, meaning that anything
* that takes a lock less than AccessExclusiveLock can change object definitions
* while pg_dump is running. Be careful to check that the appropriate data is
* derived by pg_dump using an MVCC snapshot, rather than syscache lookups,
* otherwise we might end up with an inconsistent dump that can't restore.
*/
LOCKMODE
AlterTableGetLockLevel(List *cmds)
{
/*
* This only works if we read catalog tables using MVCC snapshots.
*/
ListCell *lcmd;
LOCKMODE lockmode = ShareUpdateExclusiveLock;
foreach(lcmd, cmds)
{
AlterTableCmd *cmd = (AlterTableCmd *) lfirst(lcmd);
LOCKMODE cmd_lockmode = AccessExclusiveLock; /* default for compiler */
switch (cmd->subtype)
{
/*
* These subcommands rewrite the heap, so require full locks.
*/
case AT_AddColumn: /* may rewrite heap, in some cases and visible
* to SELECT */
case AT_SetAccessMethod: /* must rewrite heap */
case AT_SetTableSpace: /* must rewrite heap */
case AT_AlterColumnType: /* must rewrite heap */
cmd_lockmode = AccessExclusiveLock;
break;
/*
* These subcommands may require addition of toast tables. If
* we add a toast table to a table currently being scanned, we
* might miss data added to the new toast table by concurrent
* insert transactions.
*/
case AT_SetStorage: /* may add toast tables, see
* ATRewriteCatalogs() */
cmd_lockmode = AccessExclusiveLock;
break;
/*
* Removing constraints can affect SELECTs that have been
* optimized assuming the constraint holds true. See also
* CloneFkReferenced.
*/
case AT_DropConstraint: /* as DROP INDEX */
case AT_DropNotNull: /* may change some SQL plans */
cmd_lockmode = AccessExclusiveLock;
break;
/*
* Subcommands that may be visible to concurrent SELECTs
*/
case AT_DropColumn: /* change visible to SELECT */
case AT_AddColumnToView: /* CREATE VIEW */
case AT_DropOids: /* used to equiv to DropColumn */
case AT_EnableAlwaysRule: /* may change SELECT rules */
case AT_EnableReplicaRule: /* may change SELECT rules */
case AT_EnableRule: /* may change SELECT rules */
case AT_DisableRule: /* may change SELECT rules */
cmd_lockmode = AccessExclusiveLock;
break;
/*
* Changing owner may remove implicit SELECT privileges
*/
case AT_ChangeOwner: /* change visible to SELECT */
cmd_lockmode = AccessExclusiveLock;
break;
/*
* Changing foreign table options may affect optimization.
*/
case AT_GenericOptions:
case AT_AlterColumnGenericOptions:
cmd_lockmode = AccessExclusiveLock;
break;
/*
* These subcommands affect write operations only.
*/
case AT_EnableTrig:
case AT_EnableAlwaysTrig:
case AT_EnableReplicaTrig:
case AT_EnableTrigAll:
case AT_EnableTrigUser:
case AT_DisableTrig:
case AT_DisableTrigAll:
case AT_DisableTrigUser:
cmd_lockmode = ShareRowExclusiveLock;
break;
/*
* These subcommands affect write operations only. XXX
* Theoretically, these could be ShareRowExclusiveLock.
*/
case AT_ColumnDefault:
case AT_CookedColumnDefault:
case AT_AlterConstraint:
case AT_AddIndex: /* from ADD CONSTRAINT */
case AT_AddIndexConstraint:
case AT_ReplicaIdentity:
case AT_SetNotNull:
case AT_SetAttNotNull:
case AT_EnableRowSecurity:
case AT_DisableRowSecurity:
case AT_ForceRowSecurity:
case AT_NoForceRowSecurity:
case AT_AddIdentity:
case AT_DropIdentity:
case AT_SetIdentity:
case AT_SetExpression:
case AT_DropExpression:
case AT_SetCompression:
cmd_lockmode = AccessExclusiveLock;
break;
case AT_AddConstraint:
case AT_ReAddConstraint: /* becomes AT_AddConstraint */
case AT_ReAddDomainConstraint: /* becomes AT_AddConstraint */
if (IsA(cmd->def, Constraint))
{
Constraint *con = (Constraint *) cmd->def;
switch (con->contype)
{
case CONSTR_EXCLUSION:
case CONSTR_PRIMARY:
case CONSTR_UNIQUE:
/*
* Cases essentially the same as CREATE INDEX. We
* could reduce the lock strength to ShareLock if
* we can work out how to allow concurrent catalog
* updates. XXX Might be set down to
* ShareRowExclusiveLock but requires further
* analysis.
*/
cmd_lockmode = AccessExclusiveLock;
break;
case CONSTR_FOREIGN:
/*
* We add triggers to both tables when we add a
* Foreign Key, so the lock level must be at least
* as strong as CREATE TRIGGER.
*/
cmd_lockmode = ShareRowExclusiveLock;
break;
default:
cmd_lockmode = AccessExclusiveLock;
}
}
break;
/*
* These subcommands affect inheritance behaviour. Queries
* started before us will continue to see the old inheritance
* behaviour, while queries started after we commit will see
* new behaviour. No need to prevent reads or writes to the
* subtable while we hook it up though. Changing the TupDesc
* may be a problem, so keep highest lock.
*/
case AT_AddInherit:
case AT_DropInherit:
cmd_lockmode = AccessExclusiveLock;
break;
/*
* These subcommands affect implicit row type conversion. They
* have affects similar to CREATE/DROP CAST on queries. don't
* provide for invalidating parse trees as a result of such
* changes, so we keep these at AccessExclusiveLock.
*/
case AT_AddOf:
case AT_DropOf:
cmd_lockmode = AccessExclusiveLock;
break;
/*
* Only used by CREATE OR REPLACE VIEW which must conflict
* with an SELECTs currently using the view.
*/
case AT_ReplaceRelOptions:
cmd_lockmode = AccessExclusiveLock;
break;
/*
* These subcommands affect general strategies for performance
* and maintenance, though don't change the semantic results
* from normal data reads and writes. Delaying an ALTER TABLE
* behind currently active writes only delays the point where
* the new strategy begins to take effect, so there is no
* benefit in waiting. In this case the minimum restriction
* applies: we don't currently allow concurrent catalog
* updates.
*/
case AT_SetStatistics: /* Uses MVCC in getTableAttrs() */
case AT_ClusterOn: /* Uses MVCC in getIndexes() */
case AT_DropCluster: /* Uses MVCC in getIndexes() */
case AT_SetOptions: /* Uses MVCC in getTableAttrs() */
case AT_ResetOptions: /* Uses MVCC in getTableAttrs() */
cmd_lockmode = ShareUpdateExclusiveLock;
break;
case AT_SetLogged:
case AT_SetUnLogged:
cmd_lockmode = AccessExclusiveLock;
break;
case AT_ValidateConstraint: /* Uses MVCC in getConstraints() */
cmd_lockmode = ShareUpdateExclusiveLock;
break;
/*
* Rel options are more complex than first appears. Options
* are set here for tables, views and indexes; for historical
* reasons these can all be used with ALTER TABLE, so we can't
* decide between them using the basic grammar.
*/
case AT_SetRelOptions: /* Uses MVCC in getIndexes() and
* getTables() */
case AT_ResetRelOptions: /* Uses MVCC in getIndexes() and
* getTables() */
cmd_lockmode = AlterTableGetRelOptionsLockLevel((List *) cmd->def);
break;
case AT_AttachPartition:
cmd_lockmode = ShareUpdateExclusiveLock;
break;
case AT_DetachPartition:
if (((PartitionCmd *) cmd->def)->concurrent)
cmd_lockmode = ShareUpdateExclusiveLock;
else
cmd_lockmode = AccessExclusiveLock;
break;
case AT_DetachPartitionFinalize:
cmd_lockmode = ShareUpdateExclusiveLock;
break;
case AT_SplitPartition:
cmd_lockmode = AccessExclusiveLock;
break;
case AT_MergePartitions:
cmd_lockmode = AccessExclusiveLock;
break;
default: /* oops */
elog(ERROR, "unrecognized alter table type: %d",
(int) cmd->subtype);
break;
}
/*
* Take the greatest lockmode from any subcommand
*/
if (cmd_lockmode > lockmode)
lockmode = cmd_lockmode;
}
return lockmode;
}
/*
* ATController provides top level control over the phases.
*
* parsetree is passed in to allow it to be passed to event triggers
* when requested.
*/
static void
ATController(AlterTableStmt *parsetree,
Relation rel, List *cmds, bool recurse, LOCKMODE lockmode,
AlterTableUtilityContext *context)
{
List *wqueue = NIL;
ListCell *lcmd;
/* Phase 1: preliminary examination of commands, create work queue */
foreach(lcmd, cmds)
{
AlterTableCmd *cmd = (AlterTableCmd *) lfirst(lcmd);
ATPrepCmd(&wqueue, rel, cmd, recurse, false, lockmode, context);
}
/* Close the relation, but keep lock until commit */
relation_close(rel, NoLock);
/* Phase 2: update system catalogs */
ATRewriteCatalogs(&wqueue, lockmode, context);
/* Phase 3: scan/rewrite tables as needed, and run afterStmts */
ATRewriteTables(parsetree, &wqueue, lockmode, context);
}
/*
* ATPrepCmd
*
* Traffic cop for ALTER TABLE Phase 1 operations, including simple
* recursion and permission checks.
*
* Caller must have acquired appropriate lock type on relation already.
* This lock should be held until commit.
*/
static void
ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode,
AlterTableUtilityContext *context)
{
AlteredTableInfo *tab;
AlterTablePass pass = AT_PASS_UNSET;
/* Find or create work queue entry for this table */
tab = ATGetQueueEntry(wqueue, rel);
/*
* Disallow any ALTER TABLE other than ALTER TABLE DETACH FINALIZE on
* partitions that are pending detach.
*/
if (rel->rd_rel->relispartition &&
cmd->subtype != AT_DetachPartitionFinalize &&
PartitionHasPendingDetach(RelationGetRelid(rel)))
ereport(ERROR,
errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot alter partition \"%s\" with an incomplete detach",
RelationGetRelationName(rel)),
errhint("Use ALTER TABLE ... DETACH PARTITION ... FINALIZE to complete the pending detach operation."));
/*
* Copy the original subcommand for each table, so we can scribble on it.
* This avoids conflicts when different child tables need to make
* different parse transformations (for example, the same column may have
* different column numbers in different children).
*/
cmd = copyObject(cmd);
/*
* Do permissions and relkind checking, recursion to child tables if
* needed, and any additional phase-1 processing needed. (But beware of
* adding any processing that looks at table details that another
* subcommand could change. In some cases we reject multiple subcommands
* that could try to change the same state in contrary ways.)
*/
switch (cmd->subtype)
{
case AT_AddColumn: /* ADD COLUMN */
ATSimplePermissions(cmd->subtype, rel,
ATT_TABLE | ATT_COMPOSITE_TYPE | ATT_FOREIGN_TABLE);
ATPrepAddColumn(wqueue, rel, recurse, recursing, false, cmd,
lockmode, context);
/* Recursion occurs during execution phase */
pass = AT_PASS_ADD_COL;
break;
case AT_AddColumnToView: /* add column via CREATE OR REPLACE VIEW */
ATSimplePermissions(cmd->subtype, rel, ATT_VIEW);
ATPrepAddColumn(wqueue, rel, recurse, recursing, true, cmd,
lockmode, context);
/* Recursion occurs during execution phase */
pass = AT_PASS_ADD_COL;
break;
case AT_ColumnDefault: /* ALTER COLUMN DEFAULT */
/*
* We allow defaults on views so that INSERT into a view can have
* default-ish behavior. This works because the rewriter
* substitutes default values into INSERTs before it expands
* rules.
*/
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_VIEW | ATT_FOREIGN_TABLE);
ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context);
/* No command-specific prep needed */
pass = cmd->def ? AT_PASS_ADD_OTHERCONSTR : AT_PASS_DROP;
break;
case AT_CookedColumnDefault: /* add a pre-cooked default */
/* This is currently used only in CREATE TABLE */
/* (so the permission check really isn't necessary) */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
/* This command never recurses */
pass = AT_PASS_ADD_OTHERCONSTR;
break;
case AT_AddIdentity:
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_VIEW | ATT_FOREIGN_TABLE);
/* Set up recursion for phase 2; no other prep needed */
if (recurse)
cmd->recurse = true;
pass = AT_PASS_ADD_OTHERCONSTR;
break;
case AT_SetIdentity:
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_VIEW | ATT_FOREIGN_TABLE);
/* Set up recursion for phase 2; no other prep needed */
if (recurse)
cmd->recurse = true;
/* This should run after AddIdentity, so do it in MISC pass */
pass = AT_PASS_MISC;
break;
case AT_DropIdentity:
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_VIEW | ATT_FOREIGN_TABLE);
/* Set up recursion for phase 2; no other prep needed */
if (recurse)
cmd->recurse = true;
pass = AT_PASS_DROP;
break;
case AT_DropNotNull: /* ALTER COLUMN DROP NOT NULL */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
/* Set up recursion for phase 2; no other prep needed */
if (recurse)
cmd->recurse = true;
pass = AT_PASS_DROP;
break;
case AT_SetNotNull: /* ALTER COLUMN SET NOT NULL */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
/* Set up recursion for phase 2; no other prep needed */
if (recurse)
cmd->recurse = true;
pass = AT_PASS_COL_ATTRS;
break;
case AT_SetAttNotNull: /* set pg_attribute.attnotnull without adding
* a constraint */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
/* Need command-specific recursion decision */
ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context);
pass = AT_PASS_COL_ATTRS;
break;
case AT_SetExpression: /* ALTER COLUMN SET EXPRESSION */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context);
pass = AT_PASS_SET_EXPRESSION;
break;
case AT_DropExpression: /* ALTER COLUMN DROP EXPRESSION */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context);
ATPrepDropExpression(rel, cmd, recurse, recursing, lockmode);
pass = AT_PASS_DROP;
break;
case AT_SetStatistics: /* ALTER COLUMN SET STATISTICS */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_MATVIEW | ATT_INDEX | ATT_PARTITIONED_INDEX | ATT_FOREIGN_TABLE);
ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context);
/* No command-specific prep needed */
pass = AT_PASS_MISC;
break;
case AT_SetOptions: /* ALTER COLUMN SET ( options ) */
case AT_ResetOptions: /* ALTER COLUMN RESET ( options ) */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_MATVIEW | ATT_FOREIGN_TABLE);
/* This command never recurses */
pass = AT_PASS_MISC;
break;
case AT_SetStorage: /* ALTER COLUMN SET STORAGE */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_MATVIEW | ATT_FOREIGN_TABLE);
ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context);
/* No command-specific prep needed */
pass = AT_PASS_MISC;
break;
case AT_SetCompression: /* ALTER COLUMN SET COMPRESSION */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_MATVIEW);
/* This command never recurses */
/* No command-specific prep needed */
pass = AT_PASS_MISC;
break;
case AT_DropColumn: /* DROP COLUMN */
ATSimplePermissions(cmd->subtype, rel,
ATT_TABLE | ATT_COMPOSITE_TYPE | ATT_FOREIGN_TABLE);
ATPrepDropColumn(wqueue, rel, recurse, recursing, cmd,
lockmode, context);
/* Recursion occurs during execution phase */
pass = AT_PASS_DROP;
break;
case AT_AddIndex: /* ADD INDEX */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE);
/* This command never recurses */
/* No command-specific prep needed */
pass = AT_PASS_ADD_INDEX;
break;
case AT_AddConstraint: /* ADD CONSTRAINT */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
/* Recursion occurs during execution phase */
/* No command-specific prep needed except saving recurse flag */
if (recurse)
cmd->recurse = true;
pass = AT_PASS_ADD_CONSTR;
break;
case AT_AddIndexConstraint: /* ADD CONSTRAINT USING INDEX */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE);
/* This command never recurses */
/* No command-specific prep needed */
pass = AT_PASS_ADD_INDEXCONSTR;
break;
case AT_DropConstraint: /* DROP CONSTRAINT */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
ATCheckPartitionsNotInUse(rel, lockmode);
/* Other recursion occurs during execution phase */
/* No command-specific prep needed except saving recurse flag */
if (recurse)
cmd->recurse = true;
pass = AT_PASS_DROP;
break;
case AT_AlterColumnType: /* ALTER COLUMN TYPE */
ATSimplePermissions(cmd->subtype, rel,
ATT_TABLE | ATT_COMPOSITE_TYPE | ATT_FOREIGN_TABLE);
/* See comments for ATPrepAlterColumnType */
cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, recurse, lockmode,
AT_PASS_UNSET, context);
Assert(cmd != NULL);
/* Performs own recursion */
ATPrepAlterColumnType(wqueue, tab, rel, recurse, recursing, cmd,
lockmode, context);
pass = AT_PASS_ALTER_TYPE;
break;
case AT_AlterColumnGenericOptions:
ATSimplePermissions(cmd->subtype, rel, ATT_FOREIGN_TABLE);
/* This command never recurses */
/* No command-specific prep needed */
pass = AT_PASS_MISC;
break;
case AT_ChangeOwner: /* ALTER OWNER */
/* This command never recurses */
/* No command-specific prep needed */
pass = AT_PASS_MISC;
break;
case AT_ClusterOn: /* CLUSTER ON */
case AT_DropCluster: /* SET WITHOUT CLUSTER */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_MATVIEW);
/* These commands never recurse */
/* No command-specific prep needed */
pass = AT_PASS_MISC;
break;
case AT_SetLogged: /* SET LOGGED */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_SEQUENCE);
if (tab->chgPersistence)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot change persistence setting twice")));
tab->chgPersistence = ATPrepChangePersistence(rel, true);
/* force rewrite if necessary; see comment in ATRewriteTables */
if (tab->chgPersistence)
{
tab->rewrite |= AT_REWRITE_ALTER_PERSISTENCE;
tab->newrelpersistence = RELPERSISTENCE_PERMANENT;
}
pass = AT_PASS_MISC;
break;
case AT_SetUnLogged: /* SET UNLOGGED */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_SEQUENCE);
if (tab->chgPersistence)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot change persistence setting twice")));
tab->chgPersistence = ATPrepChangePersistence(rel, false);
/* force rewrite if necessary; see comment in ATRewriteTables */
if (tab->chgPersistence)
{
tab->rewrite |= AT_REWRITE_ALTER_PERSISTENCE;
tab->newrelpersistence = RELPERSISTENCE_UNLOGGED;
}
pass = AT_PASS_MISC;
break;
case AT_DropOids: /* SET WITHOUT OIDS */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
pass = AT_PASS_DROP;
break;
case AT_SetAccessMethod: /* SET ACCESS METHOD */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_MATVIEW);
/* check if another access method change was already requested */
if (tab->chgAccessMethod)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot have multiple SET ACCESS METHOD subcommands")));
ATPrepSetAccessMethod(tab, rel, cmd->name);
pass = AT_PASS_MISC; /* does not matter; no work in Phase 2 */
break;
case AT_SetTableSpace: /* SET TABLESPACE */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_MATVIEW | ATT_INDEX |
ATT_PARTITIONED_INDEX);
/* This command never recurses */
ATPrepSetTableSpace(tab, rel, cmd->name, lockmode);
pass = AT_PASS_MISC; /* doesn't actually matter */
break;
case AT_SetRelOptions: /* SET (...) */
case AT_ResetRelOptions: /* RESET (...) */
case AT_ReplaceRelOptions: /* reset them all, then set just these */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_VIEW | ATT_MATVIEW | ATT_INDEX);
/* This command never recurses */
/* No command-specific prep needed */
pass = AT_PASS_MISC;
break;
case AT_AddInherit: /* INHERIT */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
/* This command never recurses */
ATPrepAddInherit(rel);
pass = AT_PASS_MISC;
break;
case AT_DropInherit: /* NO INHERIT */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
/* This command never recurses */
/* No command-specific prep needed */
pass = AT_PASS_MISC;
break;
case AT_AlterConstraint: /* ALTER CONSTRAINT */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE);
/* Recursion occurs during execution phase */
pass = AT_PASS_MISC;
break;
case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
/* Recursion occurs during execution phase */
/* No command-specific prep needed except saving recurse flag */
if (recurse)
cmd->recurse = true;
pass = AT_PASS_MISC;
break;
case AT_ReplicaIdentity: /* REPLICA IDENTITY ... */
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_MATVIEW);
pass = AT_PASS_MISC;
/* This command never recurses */
/* No command-specific prep needed */
break;
case AT_EnableTrig: /* ENABLE TRIGGER variants */
case AT_EnableAlwaysTrig:
case AT_EnableReplicaTrig:
case AT_EnableTrigAll:
case AT_EnableTrigUser:
case AT_DisableTrig: /* DISABLE TRIGGER variants */
case AT_DisableTrigAll:
case AT_DisableTrigUser:
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
/* Set up recursion for phase 2; no other prep needed */
if (recurse)
cmd->recurse = true;
pass = AT_PASS_MISC;
break;
case AT_EnableRule: /* ENABLE/DISABLE RULE variants */
case AT_EnableAlwaysRule:
case AT_EnableReplicaRule:
case AT_DisableRule:
case AT_AddOf: /* OF */
case AT_DropOf: /* NOT OF */
case AT_EnableRowSecurity:
case AT_DisableRowSecurity:
case AT_ForceRowSecurity:
case AT_NoForceRowSecurity:
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE);
/* These commands never recurse */
/* No command-specific prep needed */
pass = AT_PASS_MISC;
break;
case AT_GenericOptions:
ATSimplePermissions(cmd->subtype, rel, ATT_FOREIGN_TABLE);
/* No command-specific prep needed */
pass = AT_PASS_MISC;
break;
case AT_AttachPartition:
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_PARTITIONED_INDEX);
/* No command-specific prep needed */
pass = AT_PASS_MISC;
break;
case AT_DetachPartition:
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE);
/* No command-specific prep needed */
pass = AT_PASS_MISC;
break;
case AT_DetachPartitionFinalize:
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE);
/* No command-specific prep needed */
pass = AT_PASS_MISC;
break;
case AT_SplitPartition:
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE);
/* No command-specific prep needed */
pass = AT_PASS_MISC;
break;
case AT_MergePartitions:
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE);
/* No command-specific prep needed */
pass = AT_PASS_MISC;
break;
default: /* oops */
elog(ERROR, "unrecognized alter table type: %d",
(int) cmd->subtype);
pass = AT_PASS_UNSET; /* keep compiler quiet */
break;
}
Assert(pass > AT_PASS_UNSET);
/* Add the subcommand to the appropriate list for phase 2 */
tab->subcmds[pass] = lappend(tab->subcmds[pass], cmd);
}
/*
* ATRewriteCatalogs
*
* Traffic cop for ALTER TABLE Phase 2 operations. Subcommands are
* dispatched in a "safe" execution order (designed to avoid unnecessary
* conflicts).
*/
static void
ATRewriteCatalogs(List **wqueue, LOCKMODE lockmode,
AlterTableUtilityContext *context)
{
ListCell *ltab;
/*
* We process all the tables "in parallel", one pass at a time. This is
* needed because we may have to propagate work from one table to another
* (specifically, ALTER TYPE on a foreign key's PK has to dispatch the
* re-adding of the foreign key constraint to the other table). Work can
* only be propagated into later passes, however.
*/
for (AlterTablePass pass = 0; pass < AT_NUM_PASSES; pass++)
{
/* Go through each table that needs to be processed */
foreach(ltab, *wqueue)
{
AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab);
List *subcmds = tab->subcmds[pass];
ListCell *lcmd;
if (subcmds == NIL)
continue;
/*
* Open the relation and store it in tab. This allows subroutines
* close and reopen, if necessary. Appropriate lock was obtained
* by phase 1, needn't get it again.
*/
tab->rel = relation_open(tab->relid, NoLock);
foreach(lcmd, subcmds)
ATExecCmd(wqueue, tab,
lfirst_node(AlterTableCmd, lcmd),
lockmode, pass, context);
/*
* After the ALTER TYPE or SET EXPRESSION pass, do cleanup work
* (this is not done in ATExecAlterColumnType since it should be
* done only once if multiple columns of a table are altered).
*/
if (pass == AT_PASS_ALTER_TYPE || pass == AT_PASS_SET_EXPRESSION)
ATPostAlterTypeCleanup(wqueue, tab, lockmode);
if (tab->rel)
{
relation_close(tab->rel, NoLock);
tab->rel = NULL;
}
}
}
/* Check to see if a toast table must be added. */
foreach(ltab, *wqueue)
{
AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab);
/*
* If the table is source table of ATTACH PARTITION command, we did
* not modify anything about it that will change its toasting
* requirement, so no need to check.
*/
if (((tab->relkind == RELKIND_RELATION ||
tab->relkind == RELKIND_PARTITIONED_TABLE) &&
tab->partition_constraint == NULL) ||
tab->relkind == RELKIND_MATVIEW)
AlterTableCreateToastTable(tab->relid, (Datum) 0, lockmode);
}
}
/*
* ATExecCmd: dispatch a subcommand to appropriate execution routine
*/
static void
ATExecCmd(List **wqueue, AlteredTableInfo *tab,
AlterTableCmd *cmd, LOCKMODE lockmode, AlterTablePass cur_pass,
AlterTableUtilityContext *context)
{
ObjectAddress address = InvalidObjectAddress;
Relation rel = tab->rel;
switch (cmd->subtype)
{
case AT_AddColumn: /* ADD COLUMN */
case AT_AddColumnToView: /* add column via CREATE OR REPLACE VIEW */
address = ATExecAddColumn(wqueue, tab, rel, &cmd,
cmd->recurse, false,
lockmode, cur_pass, context);
break;
case AT_ColumnDefault: /* ALTER COLUMN DEFAULT */
address = ATExecColumnDefault(rel, cmd->name, cmd->def, lockmode);
break;
case AT_CookedColumnDefault: /* add a pre-cooked default */
address = ATExecCookedColumnDefault(rel, cmd->num, cmd->def);
break;
case AT_AddIdentity:
cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode,
cur_pass, context);
Assert(cmd != NULL);
address = ATExecAddIdentity(rel, cmd->name, cmd->def, lockmode, cmd->recurse, false);
break;
case AT_SetIdentity:
cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode,
cur_pass, context);
Assert(cmd != NULL);
address = ATExecSetIdentity(rel, cmd->name, cmd->def, lockmode, cmd->recurse, false);
break;
case AT_DropIdentity:
address = ATExecDropIdentity(rel, cmd->name, cmd->missing_ok, lockmode, cmd->recurse, false);
break;
case AT_DropNotNull: /* ALTER COLUMN DROP NOT NULL */
address = ATExecDropNotNull(rel, cmd->name, cmd->recurse, lockmode);
break;
case AT_SetNotNull: /* ALTER COLUMN SET NOT NULL */
address = ATExecSetNotNull(wqueue, rel, NULL, cmd->name,
cmd->recurse, false, NULL, lockmode);
break;
case AT_SetAttNotNull: /* set pg_attribute.attnotnull */
address = ATExecSetAttNotNull(wqueue, rel, cmd->name, lockmode);
break;
case AT_SetExpression:
address = ATExecSetExpression(tab, rel, cmd->name, cmd->def, lockmode);
break;
case AT_DropExpression:
address = ATExecDropExpression(rel, cmd->name, cmd->missing_ok, lockmode);
break;
case AT_SetStatistics: /* ALTER COLUMN SET STATISTICS */
address = ATExecSetStatistics(rel, cmd->name, cmd->num, cmd->def, lockmode);
break;
case AT_SetOptions: /* ALTER COLUMN SET ( options ) */
address = ATExecSetOptions(rel, cmd->name, cmd->def, false, lockmode);
break;
case AT_ResetOptions: /* ALTER COLUMN RESET ( options ) */
address = ATExecSetOptions(rel, cmd->name, cmd->def, true, lockmode);
break;
case AT_SetStorage: /* ALTER COLUMN SET STORAGE */
address = ATExecSetStorage(rel, cmd->name, cmd->def, lockmode);
break;
case AT_SetCompression: /* ALTER COLUMN SET COMPRESSION */
address = ATExecSetCompression(rel, cmd->name, cmd->def,
lockmode);
break;
case AT_DropColumn: /* DROP COLUMN */
address = ATExecDropColumn(wqueue, rel, cmd->name,
cmd->behavior, cmd->recurse, false,
cmd->missing_ok, lockmode,
NULL);
break;
case AT_AddIndex: /* ADD INDEX */
address = ATExecAddIndex(tab, rel, (IndexStmt *) cmd->def, false,
lockmode);
break;
case AT_ReAddIndex: /* ADD INDEX */
address = ATExecAddIndex(tab, rel, (IndexStmt *) cmd->def, true,
lockmode);
break;
case AT_ReAddStatistics: /* ADD STATISTICS */
address = ATExecAddStatistics(tab, rel, (CreateStatsStmt *) cmd->def,
true, lockmode);
break;
case AT_AddConstraint: /* ADD CONSTRAINT */
/* Transform the command only during initial examination */
if (cur_pass == AT_PASS_ADD_CONSTR)
cmd = ATParseTransformCmd(wqueue, tab, rel, cmd,
cmd->recurse, lockmode,
cur_pass, context);
/* Depending on constraint type, might be no more work to do now */
if (cmd != NULL)
address =
ATExecAddConstraint(wqueue, tab, rel,
(Constraint *) cmd->def,
cmd->recurse, false, lockmode);
break;
case AT_ReAddConstraint: /* Re-add pre-existing check constraint */
address =
ATExecAddConstraint(wqueue, tab, rel, (Constraint *) cmd->def,
true, true, lockmode);
break;
case AT_ReAddDomainConstraint: /* Re-add pre-existing domain check
* constraint */
address =
AlterDomainAddConstraint(((AlterDomainStmt *) cmd->def)->typeName,
((AlterDomainStmt *) cmd->def)->def,
NULL);
break;
case AT_ReAddComment: /* Re-add existing comment */
address = CommentObject((CommentStmt *) cmd->def);
break;
case AT_AddIndexConstraint: /* ADD CONSTRAINT USING INDEX */
address = ATExecAddIndexConstraint(tab, rel, (IndexStmt *) cmd->def,
lockmode);
break;
case AT_AlterConstraint: /* ALTER CONSTRAINT */
address = ATExecAlterConstraint(rel, cmd, false, false, lockmode);
break;
case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */
address = ATExecValidateConstraint(wqueue, rel, cmd->name, cmd->recurse,
false, lockmode);
break;
case AT_DropConstraint: /* DROP CONSTRAINT */
ATExecDropConstraint(rel, cmd->name, cmd->behavior,
cmd->recurse,
cmd->missing_ok, lockmode);
break;
case AT_AlterColumnType: /* ALTER COLUMN TYPE */
/* parse transformation was done earlier */
address = ATExecAlterColumnType(tab, rel, cmd, lockmode);
break;
case AT_AlterColumnGenericOptions: /* ALTER COLUMN OPTIONS */
address =
ATExecAlterColumnGenericOptions(rel, cmd->name,
(List *) cmd->def, lockmode);
break;
case AT_ChangeOwner: /* ALTER OWNER */
ATExecChangeOwner(RelationGetRelid(rel),
get_rolespec_oid(cmd->newowner, false),
false, lockmode);
break;
case AT_ClusterOn: /* CLUSTER ON */
address = ATExecClusterOn(rel, cmd->name, lockmode);
break;
case AT_DropCluster: /* SET WITHOUT CLUSTER */
ATExecDropCluster(rel, lockmode);
break;
case AT_SetLogged: /* SET LOGGED */
case AT_SetUnLogged: /* SET UNLOGGED */
break;
case AT_DropOids: /* SET WITHOUT OIDS */
/* nothing to do here, oid columns don't exist anymore */
break;
case AT_SetAccessMethod: /* SET ACCESS METHOD */
/*
* Only do this for partitioned tables, for which this is just a
* catalog change. Tables with storage are handled by Phase 3.
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
tab->chgAccessMethod)
ATExecSetAccessMethodNoStorage(rel, tab->newAccessMethod);
break;
case AT_SetTableSpace: /* SET TABLESPACE */
/*
* Only do this for partitioned tables and indexes, for which this
* is just a catalog change. Other relation types which have
* storage are handled by Phase 3.
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)
ATExecSetTableSpaceNoStorage(rel, tab->newTableSpace);
break;
case AT_SetRelOptions: /* SET (...) */
case AT_ResetRelOptions: /* RESET (...) */
case AT_ReplaceRelOptions: /* replace entire option list */
ATExecSetRelOptions(rel, (List *) cmd->def, cmd->subtype, lockmode);
break;
case AT_EnableTrig: /* ENABLE TRIGGER name */
ATExecEnableDisableTrigger(rel, cmd->name,
TRIGGER_FIRES_ON_ORIGIN, false,
cmd->recurse,
lockmode);
break;
case AT_EnableAlwaysTrig: /* ENABLE ALWAYS TRIGGER name */
ATExecEnableDisableTrigger(rel, cmd->name,
TRIGGER_FIRES_ALWAYS, false,
cmd->recurse,
lockmode);
break;
case AT_EnableReplicaTrig: /* ENABLE REPLICA TRIGGER name */
ATExecEnableDisableTrigger(rel, cmd->name,
TRIGGER_FIRES_ON_REPLICA, false,
cmd->recurse,
lockmode);
break;
case AT_DisableTrig: /* DISABLE TRIGGER name */
ATExecEnableDisableTrigger(rel, cmd->name,
TRIGGER_DISABLED, false,
cmd->recurse,
lockmode);
break;
case AT_EnableTrigAll: /* ENABLE TRIGGER ALL */
ATExecEnableDisableTrigger(rel, NULL,
TRIGGER_FIRES_ON_ORIGIN, false,
cmd->recurse,
lockmode);
break;
case AT_DisableTrigAll: /* DISABLE TRIGGER ALL */
ATExecEnableDisableTrigger(rel, NULL,
TRIGGER_DISABLED, false,
cmd->recurse,
lockmode);
break;
case AT_EnableTrigUser: /* ENABLE TRIGGER USER */
ATExecEnableDisableTrigger(rel, NULL,
TRIGGER_FIRES_ON_ORIGIN, true,
cmd->recurse,
lockmode);
break;
case AT_DisableTrigUser: /* DISABLE TRIGGER USER */
ATExecEnableDisableTrigger(rel, NULL,
TRIGGER_DISABLED, true,
cmd->recurse,
lockmode);
break;
case AT_EnableRule: /* ENABLE RULE name */
ATExecEnableDisableRule(rel, cmd->name,
RULE_FIRES_ON_ORIGIN, lockmode);
break;
case AT_EnableAlwaysRule: /* ENABLE ALWAYS RULE name */
ATExecEnableDisableRule(rel, cmd->name,
RULE_FIRES_ALWAYS, lockmode);
break;
case AT_EnableReplicaRule: /* ENABLE REPLICA RULE name */
ATExecEnableDisableRule(rel, cmd->name,
RULE_FIRES_ON_REPLICA, lockmode);
break;
case AT_DisableRule: /* DISABLE RULE name */
ATExecEnableDisableRule(rel, cmd->name,
RULE_DISABLED, lockmode);
break;
case AT_AddInherit:
address = ATExecAddInherit(rel, (RangeVar *) cmd->def, lockmode);
break;
case AT_DropInherit:
address = ATExecDropInherit(rel, (RangeVar *) cmd->def, lockmode);
break;
case AT_AddOf:
address = ATExecAddOf(rel, (TypeName *) cmd->def, lockmode);
break;
case AT_DropOf:
ATExecDropOf(rel, lockmode);
break;
case AT_ReplicaIdentity:
ATExecReplicaIdentity(rel, (ReplicaIdentityStmt *) cmd->def, lockmode);
break;
case AT_EnableRowSecurity:
ATExecSetRowSecurity(rel, true);
break;
case AT_DisableRowSecurity:
ATExecSetRowSecurity(rel, false);
break;
case AT_ForceRowSecurity:
ATExecForceNoForceRowSecurity(rel, true);
break;
case AT_NoForceRowSecurity:
ATExecForceNoForceRowSecurity(rel, false);
break;
case AT_GenericOptions:
ATExecGenericOptions(rel, (List *) cmd->def);
break;
case AT_AttachPartition:
cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode,
cur_pass, context);
Assert(cmd != NULL);
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
address = ATExecAttachPartition(wqueue, rel, (PartitionCmd *) cmd->def,
context);
else
address = ATExecAttachPartitionIdx(wqueue, rel,
((PartitionCmd *) cmd->def)->name);
break;
case AT_DetachPartition:
cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode,
cur_pass, context);
Assert(cmd != NULL);
/* ATPrepCmd ensures it must be a table */
Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
address = ATExecDetachPartition(wqueue, tab, rel,
((PartitionCmd *) cmd->def)->name,
((PartitionCmd *) cmd->def)->concurrent);
break;
case AT_DetachPartitionFinalize:
address = ATExecDetachPartitionFinalize(rel, ((PartitionCmd *) cmd->def)->name);
break;
case AT_SplitPartition:
cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode,
cur_pass, context);
Assert(cmd != NULL);
Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
ATExecSplitPartition(wqueue, tab, rel, (PartitionCmd *) cmd->def,
context);
break;
case AT_MergePartitions:
cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode,
cur_pass, context);
Assert(cmd != NULL);
Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
ATExecMergePartitions(wqueue, tab, rel, (PartitionCmd *) cmd->def,
context);
break;
default: /* oops */
elog(ERROR, "unrecognized alter table type: %d",
(int) cmd->subtype);
break;
}
/*
* Report the subcommand to interested event triggers.
*/
if (cmd)
EventTriggerCollectAlterTableSubcmd((Node *) cmd, address);
/*
* Bump the command counter to ensure the next subcommand in the sequence
* can see the changes so far
*/
CommandCounterIncrement();
}
/*
* ATParseTransformCmd: perform parse transformation for one subcommand
*
* Returns the transformed subcommand tree, if there is one, else NULL.
*
* The parser may hand back additional AlterTableCmd(s) and/or other
* utility statements, either before or after the original subcommand.
* Other AlterTableCmds are scheduled into the appropriate slot of the
* AlteredTableInfo (they had better be for later passes than the current one).
* Utility statements that are supposed to happen before the AlterTableCmd
* are executed immediately. Those that are supposed to happen afterwards
* are added to the tab->afterStmts list to be done at the very end.
*/
static AlterTableCmd *
ATParseTransformCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
AlterTableCmd *cmd, bool recurse, LOCKMODE lockmode,
AlterTablePass cur_pass, AlterTableUtilityContext *context)
{
AlterTableCmd *newcmd = NULL;
AlterTableStmt *atstmt = makeNode(AlterTableStmt);
List *beforeStmts;
List *afterStmts;
ListCell *lc;
/* Gin up an AlterTableStmt with just this subcommand and this table */
atstmt->relation =
makeRangeVar(get_namespace_name(RelationGetNamespace(rel)),
pstrdup(RelationGetRelationName(rel)),
-1);
atstmt->relation->inh = recurse;
atstmt->cmds = list_make1(cmd);
atstmt->objtype = OBJECT_TABLE; /* needn't be picky here */
atstmt->missing_ok = false;
/* Transform the AlterTableStmt */
atstmt = transformAlterTableStmt(RelationGetRelid(rel),
atstmt,
context->queryString,
&beforeStmts,
&afterStmts);
/* Execute any statements that should happen before these subcommand(s) */
foreach(lc, beforeStmts)
{
Node *stmt = (Node *) lfirst(lc);
ProcessUtilityForAlterTable(stmt, context);
CommandCounterIncrement();
}
/* Examine the transformed subcommands and schedule them appropriately */
foreach(lc, atstmt->cmds)
{
AlterTableCmd *cmd2 = lfirst_node(AlterTableCmd, lc);
AlterTablePass pass;
/*
* This switch need only cover the subcommand types that can be added
* by parse_utilcmd.c; otherwise, we'll use the default strategy of
* executing the subcommand immediately, as a substitute for the
* original subcommand. (Note, however, that this does cause
* AT_AddConstraint subcommands to be rescheduled into later passes,
* which is important for index and foreign key constraints.)
*
* We assume we needn't do any phase-1 checks for added subcommands.
*/
switch (cmd2->subtype)
{
case AT_SetAttNotNull:
ATSimpleRecursion(wqueue, rel, cmd2, recurse, lockmode, context);
pass = AT_PASS_COL_ATTRS;
break;
case AT_AddIndex:
/*
* A primary key on an inheritance parent needs supporting NOT
* NULL constraint on its children; enqueue commands to create
* those or mark them inherited if they already exist.
*/
ATPrepAddPrimaryKey(wqueue, rel, cmd2, lockmode, context);
pass = AT_PASS_ADD_INDEX;
break;
case AT_AddIndexConstraint:
/* as above */
ATPrepAddPrimaryKey(wqueue, rel, cmd2, lockmode, context);
pass = AT_PASS_ADD_INDEXCONSTR;
break;
case AT_AddConstraint:
/* Recursion occurs during execution phase */
if (recurse)
cmd2->recurse = true;
switch (castNode(Constraint, cmd2->def)->contype)
{
case CONSTR_PRIMARY:
case CONSTR_UNIQUE:
case CONSTR_EXCLUSION:
pass = AT_PASS_ADD_INDEXCONSTR;
break;
default:
pass = AT_PASS_ADD_OTHERCONSTR;
break;
}
break;
case AT_AlterColumnGenericOptions:
/* This command never recurses */
/* No command-specific prep needed */
pass = AT_PASS_MISC;
break;
default:
pass = cur_pass;
break;
}
if (pass < cur_pass)
{
/* Cannot schedule into a pass we already finished */
elog(ERROR, "ALTER TABLE scheduling failure: too late for pass %d",
pass);
}
else if (pass > cur_pass)
{
/* OK, queue it up for later */
tab->subcmds[pass] = lappend(tab->subcmds[pass], cmd2);
}
else
{
/*
* We should see at most one subcommand for the current pass,
* which is the transformed version of the original subcommand.
*/
if (newcmd == NULL && cmd->subtype == cmd2->subtype)
{
/* Found the transformed version of our subcommand */
newcmd = cmd2;
}
else
elog(ERROR, "ALTER TABLE scheduling failure: bogus item for pass %d",
pass);
}
}
/* Queue up any after-statements to happen at the end */
tab->afterStmts = list_concat(tab->afterStmts, afterStmts);
return newcmd;
}
/*
* ATRewriteTables: ALTER TABLE phase 3
*/
static void
ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
AlterTableUtilityContext *context)
{
ListCell *ltab;
/* Go through each table that needs to be checked or rewritten */
foreach(ltab, *wqueue)
{
AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab);
/* Relations without storage may be ignored here */
if (!RELKIND_HAS_STORAGE(tab->relkind))
continue;
/*
* If we change column data types, the operation has to be propagated
* to tables that use this table's rowtype as a column type.
* tab->newvals will also be non-NULL in the case where we're adding a
* column with a default. We choose to forbid that case as well,
* since composite types might eventually support defaults.
*
* (Eventually we'll probably need to check for composite type
* dependencies even when we're just scanning the table without a
* rewrite, but at the moment a composite type does not enforce any
* constraints, so it's not necessary/appropriate to enforce them just
* during ALTER.)
*/
if (tab->newvals != NIL || tab->rewrite > 0)
{
Relation rel;
rel = table_open(tab->relid, NoLock);
find_composite_type_dependencies(rel->rd_rel->reltype, rel, NULL);
table_close(rel, NoLock);
}
/*
* We only need to rewrite the table if at least one column needs to
* be recomputed, or we are changing its persistence or access method.
*
* There are two reasons for requiring a rewrite when changing
* persistence: on one hand, we need to ensure that the buffers
* belonging to each of the two relations are marked with or without
* BM_PERMANENT properly. On the other hand, since rewriting creates
* and assigns a new relfilenumber, we automatically create or drop an
* init fork for the relation as appropriate.
*/
if (tab->rewrite > 0 && tab->relkind != RELKIND_SEQUENCE)
{
/* Build a temporary relation and copy data */
Relation OldHeap;
Oid OIDNewHeap;
Oid NewAccessMethod;
Oid NewTableSpace;
char persistence;
OldHeap = table_open(tab->relid, NoLock);
/*
* We don't support rewriting of system catalogs; there are too
* many corner cases and too little benefit. In particular this
* is certainly not going to work for mapped catalogs.
*/
if (IsSystemRelation(OldHeap))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot rewrite system relation \"%s\"",
RelationGetRelationName(OldHeap))));
if (RelationIsUsedAsCatalogTable(OldHeap))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot rewrite table \"%s\" used as a catalog table",
RelationGetRelationName(OldHeap))));
/*
* Don't allow rewrite on temp tables of other backends ... their
* local buffer manager is not going to cope.
*/
if (RELATION_IS_OTHER_TEMP(OldHeap))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot rewrite temporary tables of other sessions")));
/*
* Select destination tablespace (same as original unless user
* requested a change)
*/
if (tab->newTableSpace)
NewTableSpace = tab->newTableSpace;
else
NewTableSpace = OldHeap->rd_rel->reltablespace;
/*
* Select destination access method (same as original unless user
* requested a change)
*/
if (tab->chgAccessMethod)
NewAccessMethod = tab->newAccessMethod;
else
NewAccessMethod = OldHeap->rd_rel->relam;
/*
* Select persistence of transient table (same as original unless
* user requested a change)
*/
persistence = tab->chgPersistence ?
tab->newrelpersistence : OldHeap->rd_rel->relpersistence;
table_close(OldHeap, NoLock);
/*
* Fire off an Event Trigger now, before actually rewriting the
* table.
*
* We don't support Event Trigger for nested commands anywhere,
* here included, and parsetree is given NULL when coming from
* AlterTableInternal.
*
* And fire it only once.
*/
if (parsetree)
EventTriggerTableRewrite((Node *) parsetree,
tab->relid,
tab->rewrite);
/*
* Create transient table that will receive the modified data.
*
* Ensure it is marked correctly as logged or unlogged. We have
* to do this here so that buffers for the new relfilenumber will
* have the right persistence set, and at the same time ensure
* that the original filenumbers's buffers will get read in with
* the correct setting (i.e. the original one). Otherwise a
* rollback after the rewrite would possibly result with buffers
* for the original filenumbers having the wrong persistence
* setting.
*
* NB: This relies on swap_relation_files() also swapping the
* persistence. That wouldn't work for pg_class, but that can't be
* unlogged anyway.
*/
OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
persistence, lockmode);
/*
* Copy the heap data into the new table with the desired
* modifications, and test the current data within the table
* against new constraints generated by ALTER TABLE commands.
*/
ATRewriteTable(tab, OIDNewHeap, lockmode);
/*
* Swap the physical files of the old and new heaps, then rebuild
* indexes and discard the old heap. We can use RecentXmin for
* the table's new relfrozenxid because we rewrote all the tuples
* in ATRewriteTable, so no older Xid remains in the table. Also,
* we never try to swap toast tables by content, since we have no
* interest in letting this code work on system catalogs.
*/
finish_heap_swap(tab->relid, OIDNewHeap,
false, false, true,
!OidIsValid(tab->newTableSpace),
RecentXmin,
ReadNextMultiXactId(),
persistence);
InvokeObjectPostAlterHook(RelationRelationId, tab->relid, 0);
}
else if (tab->rewrite > 0 && tab->relkind == RELKIND_SEQUENCE)
{
if (tab->chgPersistence)
SequenceChangePersistence(tab->relid, tab->newrelpersistence);
}
else
{
/*
* If required, test the current data within the table against new
* constraints generated by ALTER TABLE commands, but don't
* rebuild data.
*/
if (tab->constraints != NIL || tab->verify_new_notnull ||
tab->partition_constraint != NULL)
ATRewriteTable(tab, InvalidOid, lockmode);
/*
* If we had SET TABLESPACE but no reason to reconstruct tuples,
* just do a block-by-block copy.
*/
if (tab->newTableSpace)
ATExecSetTableSpace(tab->relid, tab->newTableSpace, lockmode);
}
/*
* Also change persistence of owned sequences, so that it matches the
* table persistence.
*/
if (tab->chgPersistence)
{
List *seqlist = getOwnedSequences(tab->relid);
ListCell *lc;
foreach(lc, seqlist)
{
Oid seq_relid = lfirst_oid(lc);
SequenceChangePersistence(seq_relid, tab->newrelpersistence);
}
}
}
/*
* Foreign key constraints are checked in a final pass, since (a) it's
* generally best to examine each one separately, and (b) it's at least
* theoretically possible that we have changed both relations of the
* foreign key, and we'd better have finished both rewrites before we try
* to read the tables.
*/
foreach(ltab, *wqueue)
{
AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab);
Relation rel = NULL;
ListCell *lcon;
/* Relations without storage may be ignored here too */
if (!RELKIND_HAS_STORAGE(tab->relkind))
continue;
foreach(lcon, tab->constraints)
{
NewConstraint *con = lfirst(lcon);
if (con->contype == CONSTR_FOREIGN)
{
Constraint *fkconstraint = (Constraint *) con->qual;
Relation refrel;
if (rel == NULL)
{
/* Long since locked, no need for another */
rel = table_open(tab->relid, NoLock);
}
refrel = table_open(con->refrelid, RowShareLock);
validateForeignKeyConstraint(fkconstraint->conname, rel, refrel,
con->refindid,
con->conid,
con->conwithperiod);
/*
* No need to mark the constraint row as validated, we did
* that when we inserted the row earlier.
*/
table_close(refrel, NoLock);
}
}
if (rel)
table_close(rel, NoLock);
}
/* Finally, run any afterStmts that were queued up */
foreach(ltab, *wqueue)
{
AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab);
ListCell *lc;
foreach(lc, tab->afterStmts)
{
Node *stmt = (Node *) lfirst(lc);
ProcessUtilityForAlterTable(stmt, context);
CommandCounterIncrement();
}
}
}
/*
* ATRewriteTable: scan or rewrite one table
*
* OIDNewHeap is InvalidOid if we don't need to rewrite
*/
static void
ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode)
{
Relation oldrel;
Relation newrel;
TupleDesc oldTupDesc;
TupleDesc newTupDesc;
bool needscan = false;
List *notnull_attrs;
int i;
ListCell *l;
EState *estate;
CommandId mycid;
BulkInsertState bistate;
int ti_options;
ExprState *partqualstate = NULL;
/*
* Open the relation(s). We have surely already locked the existing
* table.
*/
oldrel = table_open(tab->relid, NoLock);
oldTupDesc = tab->oldDesc;
newTupDesc = RelationGetDescr(oldrel); /* includes all mods */
if (OidIsValid(OIDNewHeap))
newrel = table_open(OIDNewHeap, lockmode);
else
newrel = NULL;
/*
* Prepare a BulkInsertState and options for table_tuple_insert. The FSM
* is empty, so don't bother using it.
*/
if (newrel)
{
mycid = GetCurrentCommandId(true);
bistate = GetBulkInsertState();
ti_options = TABLE_INSERT_SKIP_FSM;
}
else
{
/* keep compiler quiet about using these uninitialized */
mycid = 0;
bistate = NULL;
ti_options = 0;
}
/*
* Generate the constraint and default execution states
*/
estate = CreateExecutorState();
/* Build the needed expression execution states */
foreach(l, tab->constraints)
{
NewConstraint *con = lfirst(l);
switch (con->contype)
{
case CONSTR_CHECK:
needscan = true;
con->qualstate = ExecPrepareExpr((Expr *) con->qual, estate);
break;
case CONSTR_FOREIGN:
/* Nothing to do here */
break;
default:
elog(ERROR, "unrecognized constraint type: %d",
(int) con->contype);
}
}
/* Build expression execution states for partition check quals */
if (tab->partition_constraint)
{
needscan = true;
partqualstate = ExecPrepareExpr(tab->partition_constraint, estate);
}
foreach(l, tab->newvals)
{
NewColumnValue *ex = lfirst(l);
/* expr already planned */
ex->exprstate = ExecInitExpr((Expr *) ex->expr, NULL);
}
notnull_attrs = NIL;
if (newrel || tab->verify_new_notnull)
{
/*
* If we are rebuilding the tuples OR if we added any new but not
* verified not-null constraints, check all not-null constraints. This
* is a bit of overkill but it minimizes risk of bugs, and
* heap_attisnull is a pretty cheap test anyway.
*/
for (i = 0; i < newTupDesc->natts; i++)
{
Form_pg_attribute attr = TupleDescAttr(newTupDesc, i);
if (attr->attnotnull && !attr->attisdropped)
notnull_attrs = lappend_int(notnull_attrs, i);
}
if (notnull_attrs)
needscan = true;
}
if (newrel || needscan)
{
ExprContext *econtext;
TupleTableSlot *oldslot;
TupleTableSlot *newslot;
TableScanDesc scan;
MemoryContext oldCxt;
List *dropped_attrs = NIL;
ListCell *lc;
Snapshot snapshot;
if (newrel)
ereport(DEBUG1,
(errmsg_internal("rewriting table \"%s\"",
RelationGetRelationName(oldrel))));
else
ereport(DEBUG1,
(errmsg_internal("verifying table \"%s\"",
RelationGetRelationName(oldrel))));
if (newrel)
{
/*
* All predicate locks on the tuples or pages are about to be made
* invalid, because we move tuples around. Promote them to
* relation locks.
*/
TransferPredicateLocksToHeapRelation(oldrel);
}
econtext = GetPerTupleExprContext(estate);
/*
* Create necessary tuple slots. When rewriting, two slots are needed,
* otherwise one suffices. In the case where one slot suffices, we
* need to use the new tuple descriptor, otherwise some constraints
* can't be evaluated. Note that even when the tuple layout is the
* same and no rewrite is required, the tupDescs might not be
* (consider ADD COLUMN without a default).
*/
if (tab->rewrite)
{
Assert(newrel != NULL);
oldslot = MakeSingleTupleTableSlot(oldTupDesc,
table_slot_callbacks(oldrel));
newslot = MakeSingleTupleTableSlot(newTupDesc,
table_slot_callbacks(newrel));
/*
* Set all columns in the new slot to NULL initially, to ensure
* columns added as part of the rewrite are initialized to NULL.
* That is necessary as tab->newvals will not contain an
* expression for columns with a NULL default, e.g. when adding a
* column without a default together with a column with a default
* requiring an actual rewrite.
*/
ExecStoreAllNullTuple(newslot);
}
else
{
oldslot = MakeSingleTupleTableSlot(newTupDesc,
table_slot_callbacks(oldrel));
newslot = NULL;
}
/*
* Any attributes that are dropped according to the new tuple
* descriptor can be set to NULL. We precompute the list of dropped
* attributes to avoid needing to do so in the per-tuple loop.
*/
for (i = 0; i < newTupDesc->natts; i++)
{
if (TupleDescAttr(newTupDesc, i)->attisdropped)
dropped_attrs = lappend_int(dropped_attrs, i);
}
/*
* Scan through the rows, generating a new row if needed and then
* checking all the constraints.
*/
snapshot = RegisterSnapshot(GetLatestSnapshot());
scan = table_beginscan(oldrel, snapshot, 0, NULL);
/*
* Switch to per-tuple memory context and reset it for each tuple
* produced, so we don't leak memory.
*/
oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
while (table_scan_getnextslot(scan, ForwardScanDirection, oldslot))
{
TupleTableSlot *insertslot;
if (tab->rewrite > 0)
{
/* Extract data from old tuple */
slot_getallattrs(oldslot);
ExecClearTuple(newslot);
/* copy attributes */
memcpy(newslot->tts_values, oldslot->tts_values,
sizeof(Datum) * oldslot->tts_nvalid);
memcpy(newslot->tts_isnull, oldslot->tts_isnull,
sizeof(bool) * oldslot->tts_nvalid);
/* Set dropped attributes to null in new tuple */
foreach(lc, dropped_attrs)
newslot->tts_isnull[lfirst_int(lc)] = true;
/*
* Constraints and GENERATED expressions might reference the
* tableoid column, so fill tts_tableOid with the desired
* value. (We must do this each time, because it gets
* overwritten with newrel's OID during storing.)
*/
newslot->tts_tableOid = RelationGetRelid(oldrel);
/*
* Process supplied expressions to replace selected columns.
*
* First, evaluate expressions whose inputs come from the old
* tuple.
*/
econtext->ecxt_scantuple = oldslot;
foreach(l, tab->newvals)
{
NewColumnValue *ex = lfirst(l);
if (ex->is_generated)
continue;
newslot->tts_values[ex->attnum - 1]
= ExecEvalExpr(ex->exprstate,
econtext,
&newslot->tts_isnull[ex->attnum - 1]);
}
ExecStoreVirtualTuple(newslot);
/*
* Now, evaluate any expressions whose inputs come from the
* new tuple. We assume these columns won't reference each
* other, so that there's no ordering dependency.
*/
econtext->ecxt_scantuple = newslot;
foreach(l, tab->newvals)
{
NewColumnValue *ex = lfirst(l);
if (!ex->is_generated)
continue;
newslot->tts_values[ex->attnum - 1]
= ExecEvalExpr(ex->exprstate,
econtext,
&newslot->tts_isnull[ex->attnum - 1]);
}
insertslot = newslot;
}
else
{
/*
* If there's no rewrite, old and new table are guaranteed to
* have the same AM, so we can just use the old slot to verify
* new constraints etc.
*/
insertslot = oldslot;
}
/* Now check any constraints on the possibly-changed tuple */
econtext->ecxt_scantuple = insertslot;
foreach(l, notnull_attrs)
{
int attn = lfirst_int(l);
if (slot_attisnull(insertslot, attn + 1))
{
Form_pg_attribute attr = TupleDescAttr(newTupDesc, attn);
ereport(ERROR,
(errcode(ERRCODE_NOT_NULL_VIOLATION),
errmsg("column \"%s\" of relation \"%s\" contains null values",
NameStr(attr->attname),
RelationGetRelationName(oldrel)),
errtablecol(oldrel, attn + 1)));
}
}
foreach(l, tab->constraints)
{
NewConstraint *con = lfirst(l);
switch (con->contype)
{
case CONSTR_CHECK:
if (!ExecCheck(con->qualstate, econtext))
ereport(ERROR,
(errcode(ERRCODE_CHECK_VIOLATION),
errmsg("check constraint \"%s\" of relation \"%s\" is violated by some row",
con->name,
RelationGetRelationName(oldrel)),
errtableconstraint(oldrel, con->name)));
break;
case CONSTR_NOTNULL:
case CONSTR_FOREIGN:
/* Nothing to do here */
break;
default:
elog(ERROR, "unrecognized constraint type: %d",
(int) con->contype);
}
}
if (partqualstate && !ExecCheck(partqualstate, econtext))
{
if (tab->validate_default)
ereport(ERROR,
(errcode(ERRCODE_CHECK_VIOLATION),
errmsg("updated partition constraint for default partition \"%s\" would be violated by some row",
RelationGetRelationName(oldrel)),
errtable(oldrel)));
else
ereport(ERROR,
(errcode(ERRCODE_CHECK_VIOLATION),
errmsg("partition constraint of relation \"%s\" is violated by some row",
RelationGetRelationName(oldrel)),
errtable(oldrel)));
}
/* Write the tuple out to the new relation */
if (newrel)
table_tuple_insert(newrel, insertslot, mycid,
ti_options, bistate);
ResetExprContext(econtext);
CHECK_FOR_INTERRUPTS();
}
MemoryContextSwitchTo(oldCxt);
table_endscan(scan);
UnregisterSnapshot(snapshot);
ExecDropSingleTupleTableSlot(oldslot);
if (newslot)
ExecDropSingleTupleTableSlot(newslot);
}
FreeExecutorState(estate);
table_close(oldrel, NoLock);
if (newrel)
{
FreeBulkInsertState(bistate);
table_finish_bulk_insert(newrel, ti_options);
table_close(newrel, NoLock);
}
}
/*
* ATGetQueueEntry: find or create an entry in the ALTER TABLE work queue
*/
static AlteredTableInfo *
ATGetQueueEntry(List **wqueue, Relation rel)
{
Oid relid = RelationGetRelid(rel);
AlteredTableInfo *tab;
ListCell *ltab;
foreach(ltab, *wqueue)
{
tab = (AlteredTableInfo *) lfirst(ltab);
if (tab->relid == relid)
return tab;
}
/*
* Not there, so add it. Note that we make a copy of the relation's
* existing descriptor before anything interesting can happen to it.
*/
tab = (AlteredTableInfo *) palloc0(sizeof(AlteredTableInfo));
tab->relid = relid;
tab->rel = NULL; /* set later */
tab->relkind = rel->rd_rel->relkind;
tab->oldDesc = CreateTupleDescCopyConstr(RelationGetDescr(rel));
tab->newAccessMethod = InvalidOid;
tab->chgAccessMethod = false;
tab->newTableSpace = InvalidOid;
tab->newrelpersistence = RELPERSISTENCE_PERMANENT;
tab->chgPersistence = false;
*wqueue = lappend(*wqueue, tab);
return tab;
}
static const char *
alter_table_type_to_string(AlterTableType cmdtype)
{
switch (cmdtype)
{
case AT_AddColumn:
case AT_AddColumnToView:
return "ADD COLUMN";
case AT_ColumnDefault:
case AT_CookedColumnDefault:
return "ALTER COLUMN ... SET DEFAULT";
case AT_DropNotNull:
return "ALTER COLUMN ... DROP NOT NULL";
case AT_SetNotNull:
return "ALTER COLUMN ... SET NOT NULL";
case AT_SetAttNotNull:
return NULL; /* not real grammar */
case AT_SetExpression:
return "ALTER COLUMN ... SET EXPRESSION";
case AT_DropExpression:
return "ALTER COLUMN ... DROP EXPRESSION";
case AT_SetStatistics:
return "ALTER COLUMN ... SET STATISTICS";
case AT_SetOptions:
return "ALTER COLUMN ... SET";
case AT_ResetOptions:
return "ALTER COLUMN ... RESET";
case AT_SetStorage:
return "ALTER COLUMN ... SET STORAGE";
case AT_SetCompression:
return "ALTER COLUMN ... SET COMPRESSION";
case AT_DropColumn:
return "DROP COLUMN";
case AT_AddIndex:
case AT_ReAddIndex:
return NULL; /* not real grammar */
case AT_AddConstraint:
case AT_ReAddConstraint:
case AT_ReAddDomainConstraint:
case AT_AddIndexConstraint:
return "ADD CONSTRAINT";
case AT_AlterConstraint:
return "ALTER CONSTRAINT";
case AT_ValidateConstraint:
return "VALIDATE CONSTRAINT";
case AT_DropConstraint:
return "DROP CONSTRAINT";
case AT_ReAddComment:
return NULL; /* not real grammar */
case AT_AlterColumnType:
return "ALTER COLUMN ... SET DATA TYPE";
case AT_AlterColumnGenericOptions:
return "ALTER COLUMN ... OPTIONS";
case AT_ChangeOwner:
return "OWNER TO";
case AT_ClusterOn:
return "CLUSTER ON";
case AT_DropCluster:
return "SET WITHOUT CLUSTER";
case AT_SetAccessMethod:
return "SET ACCESS METHOD";
case AT_SetLogged:
return "SET LOGGED";
case AT_SetUnLogged:
return "SET UNLOGGED";
case AT_DropOids:
return "SET WITHOUT OIDS";
case AT_SetTableSpace:
return "SET TABLESPACE";
case AT_SetRelOptions:
return "SET";
case AT_ResetRelOptions:
return "RESET";
case AT_ReplaceRelOptions:
return NULL; /* not real grammar */
case AT_EnableTrig:
return "ENABLE TRIGGER";
case AT_EnableAlwaysTrig:
return "ENABLE ALWAYS TRIGGER";
case AT_EnableReplicaTrig:
return "ENABLE REPLICA TRIGGER";
case AT_DisableTrig:
return "DISABLE TRIGGER";
case AT_EnableTrigAll:
return "ENABLE TRIGGER ALL";
case AT_DisableTrigAll:
return "DISABLE TRIGGER ALL";
case AT_EnableTrigUser:
return "ENABLE TRIGGER USER";
case AT_DisableTrigUser:
return "DISABLE TRIGGER USER";
case AT_EnableRule:
return "ENABLE RULE";
case AT_EnableAlwaysRule:
return "ENABLE ALWAYS RULE";
case AT_EnableReplicaRule:
return "ENABLE REPLICA RULE";
case AT_DisableRule:
return "DISABLE RULE";
case AT_AddInherit:
return "INHERIT";
case AT_DropInherit:
return "NO INHERIT";
case AT_AddOf:
return "OF";
case AT_DropOf:
return "NOT OF";
case AT_ReplicaIdentity:
return "REPLICA IDENTITY";
case AT_EnableRowSecurity:
return "ENABLE ROW SECURITY";
case AT_DisableRowSecurity:
return "DISABLE ROW SECURITY";
case AT_ForceRowSecurity:
return "FORCE ROW SECURITY";
case AT_NoForceRowSecurity:
return "NO FORCE ROW SECURITY";
case AT_GenericOptions:
return "OPTIONS";
case AT_AttachPartition:
return "ATTACH PARTITION";
case AT_DetachPartition:
return "DETACH PARTITION";
case AT_DetachPartitionFinalize:
return "DETACH PARTITION ... FINALIZE";
case AT_SplitPartition:
return "SPLIT PARTITION";
case AT_MergePartitions:
return "MERGE PARTITIONS";
case AT_AddIdentity:
return "ALTER COLUMN ... ADD IDENTITY";
case AT_SetIdentity:
return "ALTER COLUMN ... SET";
case AT_DropIdentity:
return "ALTER COLUMN ... DROP IDENTITY";
case AT_ReAddStatistics:
return NULL; /* not real grammar */
}
return NULL;
}
/*
* ATSimplePermissions
*
* - Ensure that it is a relation (or possibly a view)
* - Ensure this user is the owner
* - Ensure that it is not a system table
*/
static void
ATSimplePermissions(AlterTableType cmdtype, Relation rel, int allowed_targets)
{
int actual_target;
switch (rel->rd_rel->relkind)
{
case RELKIND_RELATION:
case RELKIND_PARTITIONED_TABLE:
actual_target = ATT_TABLE;
break;
case RELKIND_VIEW:
actual_target = ATT_VIEW;
break;
case RELKIND_MATVIEW:
actual_target = ATT_MATVIEW;
break;
case RELKIND_INDEX:
actual_target = ATT_INDEX;
break;
case RELKIND_PARTITIONED_INDEX:
actual_target = ATT_PARTITIONED_INDEX;
break;
case RELKIND_COMPOSITE_TYPE:
actual_target = ATT_COMPOSITE_TYPE;
break;
case RELKIND_FOREIGN_TABLE:
actual_target = ATT_FOREIGN_TABLE;
break;
case RELKIND_SEQUENCE:
actual_target = ATT_SEQUENCE;
break;
default:
actual_target = 0;
break;
}
/* Wrong target type? */
if ((actual_target & allowed_targets) == 0)
{
const char *action_str = alter_table_type_to_string(cmdtype);
if (action_str)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
/* translator: %s is a group of some SQL keywords */
errmsg("ALTER action %s cannot be performed on relation \"%s\"",
action_str, RelationGetRelationName(rel)),
errdetail_relkind_not_supported(rel->rd_rel->relkind)));
else
/* internal error? */
elog(ERROR, "invalid ALTER action attempted on relation \"%s\"",
RelationGetRelationName(rel));
}
/* Permissions checks */
if (!object_ownercheck(RelationRelationId, RelationGetRelid(rel), GetUserId()))
aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(rel->rd_rel->relkind),
RelationGetRelationName(rel));
if (!allowSystemTableMods && IsSystemRelation(rel))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied: \"%s\" is a system catalog",
RelationGetRelationName(rel))));
}
/*
* ATSimpleRecursion
*
* Simple table recursion sufficient for most ALTER TABLE operations.
* All direct and indirect children are processed in an unspecified order.
* Note that if a child inherits from the original table via multiple
* inheritance paths, it will be visited just once.
*/
static void
ATSimpleRecursion(List **wqueue, Relation rel,
AlterTableCmd *cmd, bool recurse, LOCKMODE lockmode,
AlterTableUtilityContext *context)
{
/*
* Propagate to children, if desired and if there are (or might be) any
* children.
*/
if (recurse && rel->rd_rel->relhassubclass)
{
Oid relid = RelationGetRelid(rel);
ListCell *child;
List *children;
children = find_all_inheritors(relid, lockmode, NULL);
/*
* 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.
*/
foreach(child, children)
{
Oid childrelid = lfirst_oid(child);
Relation childrel;
if (childrelid == relid)
continue;
/* find_all_inheritors already got lock */
childrel = relation_open(childrelid, NoLock);
CheckTableNotInUse(childrel, "ALTER TABLE");
ATPrepCmd(wqueue, childrel, cmd, false, true, lockmode, context);
relation_close(childrel, NoLock);
}
}
}
/*
* Obtain list of partitions of the given table, locking them all at the given
* lockmode and ensuring that they all pass CheckTableNotInUse.
*
* This function is a no-op if the given relation is not a partitioned table;
* in particular, nothing is done if it's a legacy inheritance parent.
*/
static void
ATCheckPartitionsNotInUse(Relation rel, LOCKMODE lockmode)
{
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
List *inh;
ListCell *cell;
inh = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL);
/* first element is the parent rel; must ignore it */
for_each_from(cell, inh, 1)
{
Relation childrel;
/* find_all_inheritors already got lock */
childrel = table_open(lfirst_oid(cell), NoLock);
CheckTableNotInUse(childrel, "ALTER TABLE");
table_close(childrel, NoLock);
}
list_free(inh);
}
}
/*
* ATTypedTableRecursion
*
* Propagate ALTER TYPE operations to the typed tables of that type.
* Also check the RESTRICT/CASCADE behavior. Given CASCADE, also permit
* recursion to inheritance children of the typed tables.
*/
static void
ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd,
LOCKMODE lockmode, AlterTableUtilityContext *context)
{
ListCell *child;
List *children;
Assert(rel->rd_rel->relkind == RELKIND_COMPOSITE_TYPE);
children = find_typed_table_dependencies(rel->rd_rel->reltype,
RelationGetRelationName(rel),
cmd->behavior);
foreach(child, children)
{
Oid childrelid = lfirst_oid(child);
Relation childrel;
childrel = relation_open(childrelid, lockmode);
CheckTableNotInUse(childrel, "ALTER TABLE");
ATPrepCmd(wqueue, childrel, cmd, true, true, lockmode, context);
relation_close(childrel, NoLock);
}
}
/*
* find_composite_type_dependencies
*
* Check to see if the type "typeOid" is being used as a column in some table
* (possibly nested several levels deep in composite types, arrays, etc!).
* Eventually, we'd like to propagate the check or rewrite operation
* into such tables, but for now, just error out if we find any.
*
* Caller should provide either the associated relation of a rowtype,
* or a type name (not both) for use in the error message, if any.
*
* Note that "typeOid" is not necessarily a composite type; it could also be
* another container type such as an array or range, or a domain over one of
* these things. The name of this function is therefore somewhat historical,
* but it's not worth changing.
*
* We assume that functions and views depending on the type are not reasons
* to reject the ALTER. (How safe is this really?)
*/
void
find_composite_type_dependencies(Oid typeOid, Relation origRelation,
const char *origTypeName)
{
Relation depRel;
ScanKeyData key[2];
SysScanDesc depScan;
HeapTuple depTup;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
/*
* We scan pg_depend to find those things that depend on the given type.
* (We assume we can ignore refobjsubid for a type.)
*/
depRel = table_open(DependRelationId, AccessShareLock);
ScanKeyInit(&key[0],
Anum_pg_depend_refclassid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(TypeRelationId));
ScanKeyInit(&key[1],
Anum_pg_depend_refobjid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(typeOid));
depScan = systable_beginscan(depRel, DependReferenceIndexId, true,
NULL, 2, key);
while (HeapTupleIsValid(depTup = systable_getnext(depScan)))
{
Form_pg_depend pg_depend = (Form_pg_depend) GETSTRUCT(depTup);
Relation rel;
TupleDesc tupleDesc;
Form_pg_attribute att;
/* Check for directly dependent types */
if (pg_depend->classid == TypeRelationId)
{
/*
* This must be an array, domain, or range containing the given
* type, so recursively check for uses of this type. Note that
* any error message will mention the original type not the
* container; this is intentional.
*/
find_composite_type_dependencies(pg_depend->objid,
origRelation, origTypeName);
continue;
}
/* Else, ignore dependees that aren't relations */
if (pg_depend->classid != RelationRelationId)
continue;
rel = relation_open(pg_depend->objid, AccessShareLock);
tupleDesc = RelationGetDescr(rel);
/*
* If objsubid identifies a specific column, refer to that in error
* messages. Otherwise, search to see if there's a user column of the
* type. (We assume system columns are never of interesting types.)
* The search is needed because an index containing an expression
* column of the target type will just be recorded as a whole-relation
* dependency. If we do not find a column of the type, the dependency
* must indicate that the type is transiently referenced in an index
* expression but not stored on disk, which we assume is OK, just as
* we do for references in views. (It could also be that the target
* type is embedded in some container type that is stored in an index
* column, but the previous recursion should catch such cases.)
*/
if (pg_depend->objsubid > 0 && pg_depend->objsubid <= tupleDesc->natts)
att = TupleDescAttr(tupleDesc, pg_depend->objsubid - 1);
else
{
att = NULL;
for (int attno = 1; attno <= tupleDesc->natts; attno++)
{
att = TupleDescAttr(tupleDesc, attno - 1);
if (att->atttypid == typeOid && !att->attisdropped)
break;
att = NULL;
}
if (att == NULL)
{
/* No such column, so assume OK */
relation_close(rel, AccessShareLock);
continue;
}
}
/*
* We definitely should reject if the relation has storage. If it's
* partitioned, then perhaps we don't have to reject: if there are
* partitions then we'll fail when we find one, else there is no
* stored data to worry about. However, it's possible that the type
* change would affect conclusions about whether the type is sortable
* or hashable and thus (if it's a partitioning column) break the
* partitioning rule. For now, reject for partitioned rels too.
*/
if (RELKIND_HAS_STORAGE(rel->rd_rel->relkind) ||
RELKIND_HAS_PARTITIONS(rel->rd_rel->relkind))
{
if (origTypeName)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot alter type \"%s\" because column \"%s.%s\" uses it",
origTypeName,
RelationGetRelationName(rel),
NameStr(att->attname))));
else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot alter type \"%s\" because column \"%s.%s\" uses it",
RelationGetRelationName(origRelation),
RelationGetRelationName(rel),
NameStr(att->attname))));
else if (origRelation->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot alter foreign table \"%s\" because column \"%s.%s\" uses its row type",
RelationGetRelationName(origRelation),
RelationGetRelationName(rel),
NameStr(att->attname))));
else
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot alter table \"%s\" because column \"%s.%s\" uses its row type",
RelationGetRelationName(origRelation),
RelationGetRelationName(rel),
NameStr(att->attname))));
}
else if (OidIsValid(rel->rd_rel->reltype))
{
/*
* A view or composite type itself isn't a problem, but we must
* recursively check for indirect dependencies via its rowtype.
*/
find_composite_type_dependencies(rel->rd_rel->reltype,
origRelation, origTypeName);
}
relation_close(rel, AccessShareLock);
}
systable_endscan(depScan);
relation_close(depRel, AccessShareLock);
}
/*
* find_typed_table_dependencies
*
* Check to see if a composite type is being used as the type of a
* typed table. Abort if any are found and behavior is RESTRICT.
* Else return the list of tables.
*/
static List *
find_typed_table_dependencies(Oid typeOid, const char *typeName, DropBehavior behavior)
{
Relation classRel;
ScanKeyData key[1];
TableScanDesc scan;
HeapTuple tuple;
List *result = NIL;
classRel = table_open(RelationRelationId, AccessShareLock);
ScanKeyInit(&key[0],
Anum_pg_class_reloftype,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(typeOid));
scan = table_beginscan_catalog(classRel, 1, key);
while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class classform = (Form_pg_class) GETSTRUCT(tuple);
if (behavior == DROP_RESTRICT)
ereport(ERROR,
(errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST),
errmsg("cannot alter type \"%s\" because it is the type of a typed table",
typeName),
errhint("Use ALTER ... CASCADE to alter the typed tables too.")));
else
result = lappend_oid(result, classform->oid);
}
table_endscan(scan);
table_close(classRel, AccessShareLock);
return result;
}
/*
* check_of_type
*
* Check whether a type is suitable for CREATE TABLE OF/ALTER TABLE OF. If it
* isn't suitable, throw an error. Currently, we require that the type
* originated with CREATE TYPE AS. We could support any row type, but doing so
* would require handling a number of extra corner cases in the DDL commands.
* (Also, allowing domain-over-composite would open up a can of worms about
* whether and how the domain's constraints should apply to derived tables.)
*/
void
check_of_type(HeapTuple typetuple)
{
Form_pg_type typ = (Form_pg_type) GETSTRUCT(typetuple);
bool typeOk = false;
if (typ->typtype == TYPTYPE_COMPOSITE)
{
Relation typeRelation;
Assert(OidIsValid(typ->typrelid));
typeRelation = relation_open(typ->typrelid, AccessShareLock);
typeOk = (typeRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE);
/*
* Close the parent rel, but keep our AccessShareLock on it until xact
* commit. That will prevent someone else from deleting or ALTERing
* the type before the typed table creation/conversion commits.
*/
relation_close(typeRelation, NoLock);
}
if (!typeOk)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("type %s is not a composite type",
format_type_be(typ->oid))));
}
/*
* ALTER TABLE ADD COLUMN
*
* Adds an additional attribute to a relation making the assumption that
* CHECK, NOT NULL, and FOREIGN KEY constraints will be removed from the
* AT_AddColumn AlterTableCmd by parse_utilcmd.c and added as independent
* AlterTableCmd's.
*
* ADD COLUMN cannot use the normal ALTER TABLE recursion mechanism, because we
* have to decide at runtime whether to recurse or not depending on whether we
* actually add a column or merely merge with an existing column. (We can't
* check this in a static pre-pass because it won't handle multiple inheritance
* situations correctly.)
*/
static void
ATPrepAddColumn(List **wqueue, Relation rel, bool recurse, bool recursing,
bool is_view, AlterTableCmd *cmd, LOCKMODE lockmode,
AlterTableUtilityContext *context)
{
if (rel->rd_rel->reloftype && !recursing)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot add column to typed table")));
if (rel->rd_rel->relkind == RELKIND_COMPOSITE_TYPE)
ATTypedTableRecursion(wqueue, rel, cmd, lockmode, context);
if (recurse && !is_view)
cmd->recurse = true;
}
/*
* Add a column to a table. The return value is the address of the
* new column in the parent relation.
*
* cmd is pass-by-ref so that we can replace it with the parse-transformed
* copy (but that happens only after we check for IF NOT EXISTS).
*/
static ObjectAddress
ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
AlterTableCmd **cmd, bool recurse, bool recursing,
LOCKMODE lockmode, AlterTablePass cur_pass,
AlterTableUtilityContext *context)
{
Oid myrelid = RelationGetRelid(rel);
ColumnDef *colDef = castNode(ColumnDef, (*cmd)->def);
bool if_not_exists = (*cmd)->missing_ok;
Relation pgclass,
attrdesc;
HeapTuple reltup;
Form_pg_attribute attribute;
int newattnum;
char relkind;
Expr *defval;
List *children;
ListCell *child;
AlterTableCmd *childcmd;
ObjectAddress address;
TupleDesc tupdesc;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
/* At top level, permission check was done in ATPrepCmd, else do it */
if (recursing)
ATSimplePermissions((*cmd)->subtype, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
if (rel->rd_rel->relispartition && !recursing)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot add column to a partition")));
attrdesc = table_open(AttributeRelationId, RowExclusiveLock);
/*
* Are we adding the column to a recursion child? If so, check whether to
* merge with an existing definition for the column. If we do merge, we
* must not recurse. Children will already have the column, and recursing
* into them would mess up attinhcount.
*/
if (colDef->inhcount > 0)
{
HeapTuple tuple;
/* Does child already have a column by this name? */
tuple = SearchSysCacheCopyAttName(myrelid, colDef->colname);
if (HeapTupleIsValid(tuple))
{
Form_pg_attribute childatt = (Form_pg_attribute) GETSTRUCT(tuple);
Oid ctypeId;
int32 ctypmod;
Oid ccollid;
/* Child column must match on type, typmod, and collation */
typenameTypeIdAndMod(NULL, colDef->typeName, &ctypeId, &ctypmod);
if (ctypeId != childatt->atttypid ||
ctypmod != childatt->atttypmod)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("child table \"%s\" has different type for column \"%s\"",
RelationGetRelationName(rel), colDef->colname)));
ccollid = GetColumnDefCollation(NULL, colDef, ctypeId);
if (ccollid != childatt->attcollation)
ereport(ERROR,
(errcode(ERRCODE_COLLATION_MISMATCH),
errmsg("child table \"%s\" has different collation for column \"%s\"",
RelationGetRelationName(rel), colDef->colname),
errdetail("\"%s\" versus \"%s\"",
get_collation_name(ccollid),
get_collation_name(childatt->attcollation))));
/* Bump the existing child att's inhcount */
childatt->attinhcount++;
if (childatt->attinhcount < 0)
ereport(ERROR,
errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("too many inheritance parents"));
CatalogTupleUpdate(attrdesc, &tuple->t_self, tuple);
heap_freetuple(tuple);
/* Inform the user about the merge */
ereport(NOTICE,
(errmsg("merging definition of column \"%s\" for child \"%s\"",
colDef->colname, RelationGetRelationName(rel))));
table_close(attrdesc, RowExclusiveLock);
/* Make the child column change visible */
CommandCounterIncrement();
return InvalidObjectAddress;
}
}
/* skip if the name already exists and if_not_exists is true */
if (!check_for_column_name_collision(rel, colDef->colname, if_not_exists))
{
table_close(attrdesc, RowExclusiveLock);
return InvalidObjectAddress;
}
/*
* Okay, we need to add the column, so go ahead and do parse
* transformation. This can result in queueing up, or even immediately
* executing, subsidiary operations (such as creation of unique indexes);
* so we mustn't do it until we have made the if_not_exists check.
*
* When recursing, the command was already transformed and we needn't do
* so again. Also, if context isn't given we can't transform. (That
* currently happens only for AT_AddColumnToView; we expect that view.c
* passed us a ColumnDef that doesn't need work.)
*/
if (context != NULL && !recursing)
{
*cmd = ATParseTransformCmd(wqueue, tab, rel, *cmd, recurse, lockmode,
cur_pass, context);
Assert(*cmd != NULL);
colDef = castNode(ColumnDef, (*cmd)->def);
}
/*
* Regular inheritance children are independent enough not to inherit the
* identity column from parent hence cannot recursively add identity
* column if the table has inheritance children.
*
* Partitions, on the other hand, are integral part of a partitioned table
* and inherit identity column. Hence propagate identity column down the
* partition hierarchy.
*/
if (colDef->identity &&
recurse &&
rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE &&
find_inheritance_children(myrelid, NoLock) != NIL)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("cannot recursively add identity column to table that has child tables")));
pgclass = table_open(RelationRelationId, RowExclusiveLock);
reltup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(myrelid));
if (!HeapTupleIsValid(reltup))
elog(ERROR, "cache lookup failed for relation %u", myrelid);
relkind = ((Form_pg_class) GETSTRUCT(reltup))->relkind;
/* Determine the new attribute's number */
newattnum = ((Form_pg_class) GETSTRUCT(reltup))->relnatts + 1;
if (newattnum > MaxHeapAttributeNumber)
ereport(ERROR,
(errcode(ERRCODE_TOO_MANY_COLUMNS),
errmsg("tables can have at most %d columns",
MaxHeapAttributeNumber)));
/*
* Construct new attribute's pg_attribute entry.
*/
tupdesc = BuildDescForRelation(list_make1(colDef));
attribute = TupleDescAttr(tupdesc, 0);
/* Fix up attribute number */
attribute->attnum = newattnum;
/* make sure datatype is legal for a column */
CheckAttributeType(NameStr(attribute->attname), attribute->atttypid, attribute->attcollation,
list_make1_oid(rel->rd_rel->reltype),
0);
InsertPgAttributeTuples(attrdesc, tupdesc, myrelid, NULL, NULL);
table_close(attrdesc, RowExclusiveLock);
/*
* Update pg_class tuple as appropriate
*/
((Form_pg_class) GETSTRUCT(reltup))->relnatts = newattnum;
CatalogTupleUpdate(pgclass, &reltup->t_self, reltup);
heap_freetuple(reltup);
/* Post creation hook for new attribute */
InvokeObjectPostCreateHook(RelationRelationId, myrelid, newattnum);
table_close(pgclass, RowExclusiveLock);
/* Make the attribute's catalog entry visible */
CommandCounterIncrement();
/*
* Store the DEFAULT, if any, in the catalogs
*/
if (colDef->raw_default)
{
RawColumnDefault *rawEnt;
rawEnt = (RawColumnDefault *) palloc(sizeof(RawColumnDefault));
rawEnt->attnum = attribute->attnum;
rawEnt->raw_default = copyObject(colDef->raw_default);
/*
* Attempt to skip a complete table rewrite by storing the specified
* DEFAULT value outside of the heap. This may be disabled inside
* AddRelationNewConstraints if the optimization cannot be applied.
*/
rawEnt->missingMode = (!colDef->generated);
rawEnt->generated = colDef->generated;
/*
* This function is intended for CREATE TABLE, so it processes a
* _list_ of defaults, but we just do one.
*/
AddRelationNewConstraints(rel, list_make1(rawEnt), NIL,
false, true, false, NULL);
/* Make the additional catalog changes visible */
CommandCounterIncrement();
/*
* Did the request for a missing value work? If not we'll have to do a
* rewrite
*/
if (!rawEnt->missingMode)
tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
}
/*
* Tell Phase 3 to fill in the default expression, if there is one.
*
* If there is no default, Phase 3 doesn't have to do anything, because
* that effectively means that the default is NULL. The heap tuple access
* routines always check for attnum > # of attributes in tuple, and return
* NULL if so, so without any modification of the tuple data we will get
* the effect of NULL values in the new column.
*
* An exception occurs when the new column is of a domain type: the domain
* might have a not-null constraint, or a check constraint that indirectly
* rejects nulls. If there are any domain constraints then we construct
* an explicit NULL default value that will be passed through
* CoerceToDomain processing. (This is a tad inefficient, since it causes
* rewriting the table which we really don't have to do, but the present
* design of domain processing doesn't offer any simple way of checking
* the constraints more directly.)
*
* Note: we use build_column_default, and not just the cooked default
* returned by AddRelationNewConstraints, so that the right thing happens
* when a datatype's default applies.
*
* Note: it might seem that this should happen at the end of Phase 2, so
* that the effects of subsequent subcommands can be taken into account.
* It's intentional that we do it now, though. The new column should be
* filled according to what is said in the ADD COLUMN subcommand, so that
* the effects are the same as if this subcommand had been run by itself
* and the later subcommands had been issued in new ALTER TABLE commands.
*
* We can skip this entirely for relations without storage, since Phase 3
* is certainly not going to touch them. System attributes don't have
* interesting defaults, either.
*/
if (RELKIND_HAS_STORAGE(relkind))
{
/*
* For an identity column, we can't use build_column_default(),
* because the sequence ownership isn't set yet. So do it manually.
*/
if (colDef->identity)
{
NextValueExpr *nve = makeNode(NextValueExpr);
nve->seqid = RangeVarGetRelid(colDef->identitySequence, NoLock, false);
nve->typeId = attribute->atttypid;
defval = (Expr *) nve;
/* must do a rewrite for identity columns */
tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
}
else
defval = (Expr *) build_column_default(rel, attribute->attnum);
if (!defval && DomainHasConstraints(attribute->atttypid))
{
Oid baseTypeId;
int32 baseTypeMod;
Oid baseTypeColl;
baseTypeMod = attribute->atttypmod;
baseTypeId = getBaseTypeAndTypmod(attribute->atttypid, &baseTypeMod);
baseTypeColl = get_typcollation(baseTypeId);
defval = (Expr *) makeNullConst(baseTypeId, baseTypeMod, baseTypeColl);
defval = (Expr *) coerce_to_target_type(NULL,
(Node *) defval,
baseTypeId,
attribute->atttypid,
attribute->atttypmod,
COERCION_ASSIGNMENT,
COERCE_IMPLICIT_CAST,
-1);
if (defval == NULL) /* should not happen */
elog(ERROR, "failed to coerce base type to domain");
}
if (defval)
{
NewColumnValue *newval;
newval = (NewColumnValue *) palloc0(sizeof(NewColumnValue));
newval->attnum = attribute->attnum;
newval->expr = expression_planner(defval);
newval->is_generated = (colDef->generated != '\0');
tab->newvals = lappend(tab->newvals, newval);
}
if (DomainHasConstraints(attribute->atttypid))
tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
if (!TupleDescAttr(rel->rd_att, attribute->attnum - 1)->atthasmissing)
{
/*
* If the new column is NOT NULL, and there is no missing value,
* tell Phase 3 it needs to check for NULLs.
*/
tab->verify_new_notnull |= colDef->is_not_null;
}
}
/*
* Add needed dependency entries for the new column.
*/
add_column_datatype_dependency(myrelid, newattnum, attribute->atttypid);
add_column_collation_dependency(myrelid, newattnum, attribute->attcollation);
/*
* 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.
*/
children =
find_inheritance_children(RelationGetRelid(rel), lockmode);
/*
* If we are told not to recurse, there had better not be any child
* tables; else the addition would put them out of step.
*/
if (children && !recurse)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("column must be added to child tables too")));
/* Children should see column as singly inherited */
if (!recursing)
{
childcmd = copyObject(*cmd);
colDef = castNode(ColumnDef, childcmd->def);
colDef->inhcount = 1;
colDef->is_local = false;
}
else
childcmd = *cmd; /* no need to copy again */
foreach(child, children)
{
Oid childrelid = lfirst_oid(child);
Relation childrel;
AlteredTableInfo *childtab;
/* find_inheritance_children already got lock */
childrel = table_open(childrelid, NoLock);
CheckTableNotInUse(childrel, "ALTER TABLE");
/* Find or create work queue entry for this table */
childtab = ATGetQueueEntry(wqueue, childrel);
/* Recurse to child; return value is ignored */
ATExecAddColumn(wqueue, childtab, childrel,
&childcmd, recurse, true,
lockmode, cur_pass, context);
table_close(childrel, NoLock);
}
ObjectAddressSubSet(address, RelationRelationId, myrelid, newattnum);
return address;
}
/*
* If a new or renamed column will collide with the name of an existing
* column and if_not_exists is false then error out, else do nothing.
*/
static bool
check_for_column_name_collision(Relation rel, const char *colname,
bool if_not_exists)
{
HeapTuple attTuple;
int attnum;
/*
* this test is deliberately not attisdropped-aware, since if one tries to
* add a column matching a dropped column name, it's gonna fail anyway.
*/
attTuple = SearchSysCache2(ATTNAME,
ObjectIdGetDatum(RelationGetRelid(rel)),
PointerGetDatum(colname));
if (!HeapTupleIsValid(attTuple))
return true;
attnum = ((Form_pg_attribute) GETSTRUCT(attTuple))->attnum;
ReleaseSysCache(attTuple);
/*
* We throw a different error message for conflicts with system column
* names, since they are normally not shown and the user might otherwise
* be confused about the reason for the conflict.
*/
if (attnum <= 0)
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_COLUMN),
errmsg("column name \"%s\" conflicts with a system column name",
colname)));
else
{
if (if_not_exists)
{
ereport(NOTICE,
(errcode(ERRCODE_DUPLICATE_COLUMN),
errmsg("column \"%s\" of relation \"%s\" already exists, skipping",
colname, RelationGetRelationName(rel))));
return false;
}
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_COLUMN),
errmsg("column \"%s\" of relation \"%s\" already exists",
colname, RelationGetRelationName(rel))));
}
return true;
}
/*
* Install a column's dependency on its datatype.
*/
static void
add_column_datatype_dependency(Oid relid, int32 attnum, Oid typid)
{
ObjectAddress myself,
referenced;
myself.classId = RelationRelationId;
myself.objectId = relid;
myself.objectSubId = attnum;
referenced.classId = TypeRelationId;
referenced.objectId = typid;
referenced.objectSubId = 0;
recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
}
/*
* Install a column's dependency on its collation.
*/
static void
add_column_collation_dependency(Oid relid, int32 attnum, Oid collid)
{
ObjectAddress myself,
referenced;
/* We know the default collation is pinned, so don't bother recording it */
if (OidIsValid(collid) && collid != DEFAULT_COLLATION_OID)
{
myself.classId = RelationRelationId;
myself.objectId = relid;
myself.objectSubId = attnum;
referenced.classId = CollationRelationId;
referenced.objectId = collid;
referenced.objectSubId = 0;
recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
}
}
/*
* ALTER TABLE ALTER COLUMN DROP NOT NULL
*
* Return the address of the modified column. If the column was already
* nullable, InvalidObjectAddress is returned.
*/
static ObjectAddress
ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
LOCKMODE lockmode)
{
HeapTuple tuple;
HeapTuple conTup;
Form_pg_attribute attTup;
AttrNumber attnum;
Relation attr_rel;
ObjectAddress address;
List *readyRels;
/*
* lookup the attribute
*/
attr_rel = table_open(AttributeRelationId, RowExclusiveLock);
tuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), colName);
if (!HeapTupleIsValid(tuple))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_COLUMN),
errmsg("column \"%s\" of relation \"%s\" does not exist",
colName, RelationGetRelationName(rel))));
attTup = (Form_pg_attribute) GETSTRUCT(tuple);
attnum = attTup->attnum;
ObjectAddressSubSet(address, RelationRelationId,
RelationGetRelid(rel), attnum);
/* If the column is already nullable there's nothing to do. */
if (!attTup->attnotnull)
{
table_close(attr_rel, RowExclusiveLock);
return InvalidObjectAddress;
}
/* Prevent them from altering a system attribute */
if (attnum <= 0)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot alter system column \"%s\"",
colName)));
if (attTup->attidentity)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("column \"%s\" of relation \"%s\" is an identity column",
colName, RelationGetRelationName(rel))));
/*
* It's not OK to remove a constraint only for the parent and leave it in
* the children, so disallow that.
*/
if (!recurse)
{
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
PartitionDesc partdesc;
partdesc = RelationGetPartitionDesc(rel, true);
if (partdesc->nparts > 0)
ereport(ERROR,
errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("cannot remove constraint from only the partitioned table when partitions exist"),
errhint("Do not specify the ONLY keyword."));
}
else if (rel->rd_rel->relhassubclass &&
find_inheritance_children(RelationGetRelid(rel), NoLock) != NIL)
{
ereport(ERROR,
errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("not-null constraint on column \"%s\" must be removed in child tables too",
colName),
errhint("Do not specify the ONLY keyword."));
}
}
/*
* If rel is partition, shouldn't drop NOT NULL if parent has the same.
*/
if (rel->rd_rel->relispartition)
{
Oid parentId = get_partition_parent(RelationGetRelid(rel), false);
Relation parent = table_open(parentId, AccessShareLock);
TupleDesc tupDesc = RelationGetDescr(parent);
AttrNumber parent_attnum;
parent_attnum = get_attnum(parentId, colName);
if (TupleDescAttr(tupDesc, parent_attnum - 1)->attnotnull)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("column \"%s\" is marked NOT NULL in parent table",
colName)));
table_close(parent, AccessShareLock);
}
/*
* Find the constraint that makes this column NOT NULL, and drop it if we
* see one. dropconstraint_internal() will do necessary consistency
* checking. If there isn't one, there are two possibilities: either the
* column is marked attnotnull because it's part of the primary key, and
* then we just throw an appropriate error; or it's a leftover marking
* that we can remove. However, before doing the latter, to avoid
* breaking consistency any further, prevent this if the column is part of
* the replica identity.
*/
conTup = findNotNullConstraint(RelationGetRelid(rel), colName);
if (conTup == NULL)
{
Bitmapset *pkcols;
Bitmapset *ircols;
/*
* If the column is in a primary key, throw a specific error message.
*/
pkcols = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_PRIMARY_KEY);
if (bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber,
pkcols))
ereport(ERROR,
errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("column \"%s\" is in a primary key", colName));
/* Also throw an error if the column is in the replica identity */
ircols = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_IDENTITY_KEY);
if (bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, ircols))
ereport(ERROR,
errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("column \"%s\" is in index used as replica identity",
get_attname(RelationGetRelid(rel), attnum, false)));
/* Otherwise, just remove the attnotnull marking and do nothing else. */
attTup->attnotnull = false;
CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
}
else
{
/* The normal case: we have a pg_constraint row, remove it */
readyRels = NIL;
dropconstraint_internal(rel, conTup, DROP_RESTRICT, recurse, false,
false, &readyRels, lockmode);
heap_freetuple(conTup);
}
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel), attnum);
table_close(attr_rel, RowExclusiveLock);
return address;
}
/*
* Helper to set pg_attribute.attnotnull if it isn't set, and to tell phase 3
* to verify it; recurses to apply the same to children.
*
* When called to alter an existing table, 'wqueue' must be given so that we can
* queue a check that existing tuples pass the constraint. When called from
* table creation, 'wqueue' should be passed as NULL.
*
* Returns true if the flag was set in any table, otherwise false.
*/
static bool
set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum, bool recurse,
LOCKMODE lockmode)
{
HeapTuple tuple;
Form_pg_attribute attForm;
bool retval = false;
/* Guard against stack overflow due to overly deep inheritance tree. */
check_stack_depth();
tuple = SearchSysCacheCopyAttNum(RelationGetRelid(rel), attnum);
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for attribute %d of relation %u",
attnum, RelationGetRelid(rel));
attForm = (Form_pg_attribute) GETSTRUCT(tuple);
if (!attForm->attnotnull)
{
Relation attr_rel;
attr_rel = table_open(AttributeRelationId, RowExclusiveLock);
attForm->attnotnull = true;
CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
table_close(attr_rel, RowExclusiveLock);
/*
* And set up for existing values to be checked, unless another
* constraint already proves this.
*/
if (wqueue && !NotNullImpliedByRelConstraints(rel, attForm))
{
AlteredTableInfo *tab;
tab = ATGetQueueEntry(wqueue, rel);
tab->verify_new_notnull = true;
}
retval = true;
}
if (recurse)
{
List *children;
ListCell *lc;
/* Make above update visible, for multiple inheritance cases */
if (retval)
CommandCounterIncrement();
children = find_inheritance_children(RelationGetRelid(rel), lockmode);
foreach(lc, children)
{
Oid childrelid = lfirst_oid(lc);
Relation childrel;
AttrNumber childattno;
/* find_inheritance_children already got lock */
childrel = table_open(childrelid, NoLock);
CheckTableNotInUse(childrel, "ALTER TABLE");
childattno = get_attnum(RelationGetRelid(childrel),
get_attname(RelationGetRelid(rel), attnum,
false));
retval |= set_attnotnull(wqueue, childrel, childattno,
recurse, lockmode);
table_close(childrel, NoLock);
}
}
return retval;
}
/*
* ALTER TABLE ALTER COLUMN SET NOT NULL
*
* Add a not-null constraint to a single table and its children. Returns
* the address of the constraint added to the parent relation, if one gets
* added, or InvalidObjectAddress otherwise.
*
* We must recurse to child tables during execution, rather than using
* ALTER TABLE's normal prep-time recursion.
*/
static ObjectAddress
ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
bool recurse, bool recursing, List **readyRels,
LOCKMODE lockmode)
{
HeapTuple tuple;
Relation constr_rel;
ScanKeyData skey;
SysScanDesc conscan;
AttrNumber attnum;
ObjectAddress address;
Constraint *constraint;
CookedConstraint *ccon;
List *cooked;
bool is_no_inherit = false;
List *ready = NIL;
/* Guard against stack overflow due to overly deep inheritance tree. */
check_stack_depth();
/*
* In cases of multiple inheritance, we might visit the same child more
* than once. In the topmost call, set up a list that we fill with all
* visited relations, to skip those.
*/
if (readyRels == NULL)
{
Assert(!recursing);
readyRels = &ready;
}
if (list_member_oid(*readyRels, RelationGetRelid(rel)))
return InvalidObjectAddress;
*readyRels = lappend_oid(*readyRels, RelationGetRelid(rel));
/* At top level, permission check was done in ATPrepCmd, else do it */
if (recursing)
{
ATSimplePermissions(AT_AddConstraint, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
Assert(conName != NULL);
}
attnum = get_attnum(RelationGetRelid(rel), colName);
if (attnum == InvalidAttrNumber)
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 */
if (attnum <= 0)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot alter system column \"%s\"",
colName)));
/* See if there's already a constraint */
constr_rel = table_open(ConstraintRelationId, RowExclusiveLock);
ScanKeyInit(&skey,
Anum_pg_constraint_conrelid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(RelationGetRelid(rel)));
conscan = systable_beginscan(constr_rel, ConstraintRelidTypidNameIndexId, true,
NULL, 1, &skey);
while (HeapTupleIsValid(tuple = systable_getnext(conscan)))
{
Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(tuple);
bool changed = false;
HeapTuple copytup;
if (conForm->contype != CONSTRAINT_NOTNULL)
continue;
if (extractNotNullColumn(tuple) != attnum)
continue;
copytup = heap_copytuple(tuple);
conForm = (Form_pg_constraint) GETSTRUCT(copytup);
/*
* If we find an appropriate constraint, we're almost done, but just
* need to change some properties on it: if we're recursing, increment
* coninhcount; if not, set conislocal if not already set.
*/
if (recursing)
{
conForm->coninhcount++;
changed = true;
}
else if (!conForm->conislocal)
{
conForm->conislocal = true;
changed = true;
}
if (changed)
{
CatalogTupleUpdate(constr_rel, &copytup->t_self, copytup);
ObjectAddressSet(address, ConstraintRelationId, conForm->oid);
}
systable_endscan(conscan);
table_close(constr_rel, RowExclusiveLock);
if (changed)
return address;
else
return InvalidObjectAddress;
}
systable_endscan(conscan);
table_close(constr_rel, RowExclusiveLock);
/*
* If we're asked not to recurse, and children exist, raise an error for
* partitioned tables. For inheritance, we act as if NO INHERIT had been
* specified.
*/
if (!recurse &&
find_inheritance_children(RelationGetRelid(rel),
NoLock) != NIL)
{
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
ereport(ERROR,
errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("constraint must be added to child tables too"),
errhint("Do not specify the ONLY keyword."));
else
is_no_inherit = true;
}
/*
* No constraint exists; we must add one. First determine a name to use,
* if we haven't already.
*/
if (!recursing)
{
Assert(conName == NULL);
conName = ChooseConstraintName(RelationGetRelationName(rel),
colName, "not_null",
RelationGetNamespace(rel),
NIL);
}
constraint = makeNode(Constraint);
constraint->contype = CONSTR_NOTNULL;
constraint->conname = conName;
constraint->deferrable = false;
constraint->initdeferred = false;
constraint->location = -1;
constraint->keys = list_make1(makeString(colName));
constraint->is_no_inherit = is_no_inherit;
constraint->inhcount = recursing ? 1 : 0;
constraint->skip_validation = false;
constraint->initially_valid = true;
/* and do it */
cooked = AddRelationNewConstraints(rel, NIL, list_make1(constraint),
false, !recursing, false, NULL);
ccon = linitial(cooked);
ObjectAddressSet(address, ConstraintRelationId, ccon->conoid);
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel), attnum);
/*
* Mark pg_attribute.attnotnull for the column. Tell that function not to
* recurse, because we're going to do it here.
*/
set_attnotnull(wqueue, rel, attnum, false, lockmode);
/*
* Recurse to propagate the constraint to children that don't have one.
*/
if (recurse)
{
List *children;
ListCell *lc;
children = find_inheritance_children(RelationGetRelid(rel),
lockmode);
foreach(lc, children)
{
Relation childrel;
childrel = table_open(lfirst_oid(lc), NoLock);
ATExecSetNotNull(wqueue, childrel,
conName, colName, recurse, true,
readyRels, lockmode);
table_close(childrel, NoLock);
}
}
return address;
}
/*
* ALTER TABLE ALTER COLUMN SET ATTNOTNULL
*
* This doesn't exist in the grammar; it's used when creating a
* primary key and the column is not already marked attnotnull.
*/
static ObjectAddress
ATExecSetAttNotNull(List **wqueue, Relation rel,
const char *colName, LOCKMODE lockmode)
{
AttrNumber attnum;
ObjectAddress address = InvalidObjectAddress;
attnum = get_attnum(RelationGetRelid(rel), colName);
if (attnum == InvalidAttrNumber)
ereport(ERROR,
errcode(ERRCODE_UNDEFINED_COLUMN),
errmsg("column \"%s\" of relation \"%s\" does not exist",
colName, RelationGetRelationName(rel)));
/*
* Make the change, if necessary, and only if so report the column as
* changed
*/
if (set_attnotnull(wqueue, rel, attnum, false, lockmode))
ObjectAddressSubSet(address, RelationRelationId,
RelationGetRelid(rel), attnum);
return address;
}
/*
* NotNullImpliedByRelConstraints
* Does rel's existing constraints imply NOT NULL for the given attribute?
*/
static bool
NotNullImpliedByRelConstraints(Relation rel, Form_pg_attribute attr)
{
NullTest *nnulltest = makeNode(NullTest);
nnulltest->arg = (Expr *) makeVar(1,
attr->attnum,
attr->atttypid,
attr->atttypmod,
attr->attcollation,
0);
nnulltest->nulltesttype = IS_NOT_NULL;
/*
* 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.
*/
nnulltest->argisrow = false;
nnulltest->location = -1;
if (ConstraintImpliedByRelConstraint(rel, list_make1(nnulltest), NIL))
{
ereport(DEBUG1,
(errmsg_internal("existing constraints on column \"%s.%s\" are sufficient to prove that it does not contain nulls",
RelationGetRelationName(rel), NameStr(attr->attname))));
return true;
}
return false;
}
/*
* ALTER TABLE ALTER COLUMN SET/DROP DEFAULT
*
* Return the address of the affected column.
*/
static ObjectAddress
ATExecColumnDefault(Relation rel, const char *colName,
Node *newDefault, LOCKMODE lockmode)
{
TupleDesc tupdesc = RelationGetDescr(rel);
AttrNumber attnum;
ObjectAddress address;
/*
* get the number of the attribute
*/
attnum = get_attnum(RelationGetRelid(rel), colName);
if (attnum == InvalidAttrNumber)
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 */
if (attnum <= 0)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot alter system column \"%s\"",
colName)));
if (TupleDescAttr(tupdesc, attnum - 1)->attidentity)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("column \"%s\" of relation \"%s\" is an identity column",
colName, RelationGetRelationName(rel)),
/* translator: %s is an SQL ALTER command */
newDefault ? 0 : errhint("Use %s instead.",
"ALTER TABLE ... ALTER COLUMN ... DROP IDENTITY")));
if (TupleDescAttr(tupdesc, attnum - 1)->attgenerated)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("column \"%s\" of relation \"%s\" is a generated column",
colName, RelationGetRelationName(rel)),
newDefault ?
/* translator: %s is an SQL ALTER command */
errhint("Use %s instead.", "ALTER TABLE ... ALTER COLUMN ... SET EXPRESSION") :
(TupleDescAttr(tupdesc, attnum - 1)->attgenerated == ATTRIBUTE_GENERATED_STORED ?
errhint("Use %s instead.", "ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION") : 0)));
/*
* Remove any old default for the column. We use RESTRICT here for
* safety, but at present we do not expect anything to depend on the
* default.
*
* We treat removing the existing default as an internal operation when it
* is preparatory to adding a new default, but as a user-initiated
* operation when the user asked for a drop.
*/
RemoveAttrDefault(RelationGetRelid(rel), attnum, DROP_RESTRICT, false,
newDefault != NULL);
if (newDefault)
{
/* SET DEFAULT */
RawColumnDefault *rawEnt;
rawEnt = (RawColumnDefault *) palloc(sizeof(RawColumnDefault));
rawEnt->attnum = attnum;
rawEnt->raw_default = newDefault;
rawEnt->missingMode = false;
rawEnt->generated = '\0';
/*
* This function is intended for CREATE TABLE, so it processes a
* _list_ of defaults, but we just do one.
*/
AddRelationNewConstraints(rel, list_make1(rawEnt), NIL,
false, true, false, NULL);
}
ObjectAddressSubSet(address, RelationRelationId,
RelationGetRelid(rel), attnum);
return address;
}
/*
* Add a pre-cooked default expression.
*
* Return the address of the affected column.
*/
static ObjectAddress
ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
Node *newDefault)
{
ObjectAddress address;
/* We assume no checking is required */
/*
* Remove any old default for the column. We use RESTRICT here for
* safety, but at present we do not expect anything to depend on the
* default. (In ordinary cases, there could not be a default in place
* anyway, but it's possible when combining LIKE with inheritance.)
*/
RemoveAttrDefault(RelationGetRelid(rel), attnum, DROP_RESTRICT, false,
true);
(void) StoreAttrDefault(rel, attnum, newDefault, true, false);
ObjectAddressSubSet(address, RelationRelationId,
RelationGetRelid(rel), attnum);
return address;
}
/*
* ALTER TABLE ALTER COLUMN ADD IDENTITY
*
* Return the address of the affected column.
*/
static ObjectAddress
ATExecAddIdentity(Relation rel, const char *colName,
Node *def, LOCKMODE lockmode, bool recurse, bool recursing)
{
Relation attrelation;
HeapTuple tuple;
Form_pg_attribute attTup;
AttrNumber attnum;
ObjectAddress address;
ColumnDef *cdef = castNode(ColumnDef, def);
bool ispartitioned;
ispartitioned = (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
if (ispartitioned && !recurse)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("cannot add identity to a column of only the partitioned table"),
errhint("Do not specify the ONLY keyword.")));
if (rel->rd_rel->relispartition && !recursing)
ereport(ERROR,
errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("cannot add identity to a column of a partition"));
attrelation = table_open(AttributeRelationId, RowExclusiveLock);
tuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), colName);
if (!HeapTupleIsValid(tuple))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_COLUMN),
errmsg("column \"%s\" of relation \"%s\" does not exist",
colName, RelationGetRelationName(rel))));
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)));
/*
* Creating a column as identity implies NOT NULL, so adding the identity
* to an existing column that is not NOT NULL would create a state that
* cannot be reproduced without contortions.
*/
if (!attTup->attnotnull)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("column \"%s\" of relation \"%s\" must be declared NOT NULL before identity can be added",
colName, RelationGetRelationName(rel))));
if (attTup->attidentity)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("column \"%s\" of relation \"%s\" is already an identity column",
colName, RelationGetRelationName(rel))));
if (attTup->atthasdef)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("column \"%s\" of relation \"%s\" already has a default value",
colName, RelationGetRelationName(rel))));
attTup->attidentity = cdef->identity;
CatalogTupleUpdate(attrelation, &tuple->t_self, tuple);
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel),
attTup->attnum);
ObjectAddressSubSet(address, RelationRelationId,
RelationGetRelid(rel), attnum);
heap_freetuple(tuple);
table_close(attrelation, RowExclusiveLock);
/*
* Recurse to propagate the identity column to partitions. Identity is
* not inherited in regular inheritance children.
*/
if (recurse && ispartitioned)
{
List *children;
ListCell *lc;
children = find_inheritance_children(RelationGetRelid(rel), lockmode);
foreach(lc, children)
{
Relation childrel;
childrel = table_open(lfirst_oid(lc), NoLock);
ATExecAddIdentity(childrel, colName, def, lockmode, recurse, true);
table_close(childrel, NoLock);
}
}
return address;
}
/*
* ALTER TABLE ALTER COLUMN SET { GENERATED or sequence options }
*
* Return the address of the affected column.
*/
static ObjectAddress
ATExecSetIdentity(Relation rel, const char *colName, Node *def,
LOCKMODE lockmode, bool recurse, bool recursing)
{
ListCell *option;
DefElem *generatedEl = NULL;
HeapTuple tuple;
Form_pg_attribute attTup;
AttrNumber attnum;
Relation attrelation;
ObjectAddress address;
bool ispartitioned;
ispartitioned = (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
if (ispartitioned && !recurse)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("cannot change identity column of only the partitioned table"),
errhint("Do not specify the ONLY keyword.")));
if (rel->rd_rel->relispartition && !recursing)
ereport(ERROR,
errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("cannot change identity column of a partition"));
foreach(option, castNode(List, def))
{
DefElem *defel = lfirst_node(DefElem, option);
if (strcmp(defel->defname, "generated") == 0)
{
if (generatedEl)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("conflicting or redundant options")));
generatedEl = defel;
}
else
elog(ERROR, "option \"%s\" not recognized",
defel->defname);
}
/*
* Even if there is nothing to change here, we run all the checks. There
* will be a subsequent ALTER SEQUENCE that relies on everything being
* there.
*/
attrelation = table_open(AttributeRelationId, RowExclusiveLock);
tuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), colName);
if (!HeapTupleIsValid(tuple))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_COLUMN),
errmsg("column \"%s\" of relation \"%s\" does not exist",
colName, RelationGetRelationName(rel))));
attTup = (Form_pg_attribute) GETSTRUCT(tuple);
attnum = attTup->attnum;
if (attnum <= 0)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot alter system column \"%s\"",
colName)));
if (!attTup->attidentity)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("column \"%s\" of relation \"%s\" is not an identity column",
colName, RelationGetRelationName(rel))));
if (generatedEl)
{
attTup->attidentity = defGetInt32(generatedEl);
CatalogTupleUpdate(attrelation, &tuple->t_self, tuple);
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel),
attTup->attnum);
ObjectAddressSubSet(address, RelationRelationId,
RelationGetRelid(rel), attnum);
}
else
address = InvalidObjectAddress;
heap_freetuple(tuple);
table_close(attrelation, RowExclusiveLock);
/*
* Recurse to propagate the identity change to partitions. Identity is not
* inherited in regular inheritance children.
*/
if (generatedEl && recurse && ispartitioned)
{
List *children;
ListCell *lc;
children = find_inheritance_children(RelationGetRelid(rel), lockmode);
foreach(lc, children)
{
Relation childrel;
childrel = table_open(lfirst_oid(lc), NoLock);
ATExecSetIdentity(childrel, colName, def, lockmode, recurse, true);
table_close(childrel, NoLock);
}
}
return address;
}
/*
* ALTER TABLE ALTER COLUMN DROP IDENTITY
*
* Return the address of the affected column.
*/
static ObjectAddress
ATExecDropIdentity(Relation rel, const char *colName, bool missing_ok, LOCKMODE lockmode,
bool recurse, bool recursing)
{
HeapTuple tuple;
Form_pg_attribute attTup;
AttrNumber attnum;
Relation attrelation;
ObjectAddress address;
Oid seqid;
ObjectAddress seqaddress;
bool ispartitioned;
ispartitioned = (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
if (ispartitioned && !recurse)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("cannot drop identity from a column of only the partitioned table"),
errhint("Do not specify the ONLY keyword.")));
if (rel->rd_rel->relispartition && !recursing)
ereport(ERROR,
errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("cannot drop identity from a column of a partition"));
attrelation = table_open(AttributeRelationId, RowExclusiveLock);
tuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), colName);
if (!HeapTupleIsValid(tuple))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_COLUMN),
errmsg("column \"%s\" of relation \"%s\" does not exist",
colName, RelationGetRelationName(rel))));
attTup = (Form_pg_attribute) GETSTRUCT(tuple);
attnum = attTup->attnum;
if (attnum <= 0)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot alter system column \"%s\"",
colName)));
if (!attTup->attidentity)
{
if (!missing_ok)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("column \"%s\" of relation \"%s\" is not an identity column",
colName, RelationGetRelationName(rel))));
else
{
ereport(NOTICE,
(errmsg("column \"%s\" of relation \"%s\" is not an identity column, skipping",
colName, RelationGetRelationName(rel))));
heap_freetuple(tuple);
table_close(attrelation, RowExclusiveLock);
return InvalidObjectAddress;
}
}
attTup->attidentity = '\0';
CatalogTupleUpdate(attrelation, &tuple->t_self, tuple);
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel),
attTup->attnum);
ObjectAddressSubSet(address, RelationRelationId,
RelationGetRelid(rel), attnum);
heap_freetuple(tuple);
table_close(attrelation, RowExclusiveLock);
/*
* Recurse to drop the identity from column in partitions. Identity is
* not inherited in regular inheritance children so ignore them.
*/
if (recurse && ispartitioned)
{
List *children;
ListCell *lc;
children = find_inheritance_children(RelationGetRelid(rel), lockmode);
foreach(lc, children)
{
Relation childrel;
childrel = table_open(lfirst_oid(lc), NoLock);
ATExecDropIdentity(childrel, colName, false, lockmode, recurse, true);
table_close(childrel, NoLock);
}
}
if (!recursing)
{
/* drop the internal sequence */
seqid = getIdentitySequence(RelationGetRelid(rel), attnum, false);
deleteDependencyRecordsForClass(RelationRelationId, seqid,
RelationRelationId, DEPENDENCY_INTERNAL);
CommandCounterIncrement();
seqaddress.classId = RelationRelationId;
seqaddress.objectId = seqid;
seqaddress.objectSubId = 0;
performDeletion(&seqaddress, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
}
return address;
}
/*
* ALTER TABLE ALTER COLUMN SET EXPRESSION
*
* Return the address of the affected column.
*/
static ObjectAddress
ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
Node *newExpr, LOCKMODE lockmode)
{
HeapTuple tuple;
Form_pg_attribute attTup;
AttrNumber attnum;
Oid attrdefoid;
ObjectAddress address;
Expr *defval;
NewColumnValue *newval;
RawColumnDefault *rawEnt;
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))));
attTup = (Form_pg_attribute) GETSTRUCT(tuple);
attnum = attTup->attnum;
if (attnum <= 0)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot alter system column \"%s\"",
colName)));
if (attTup->attgenerated != ATTRIBUTE_GENERATED_STORED)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("column \"%s\" of relation \"%s\" is not a generated column",
colName, RelationGetRelationName(rel))));
ReleaseSysCache(tuple);
/*
* Clear all the missing values if we're rewriting the table, since this
* renders them pointless.
*/
RelationClearMissing(rel);
/* make sure we don't conflict with later attribute modifications */
CommandCounterIncrement();
/*
* Find everything that depends on the column (constraints, indexes, etc),
* and record enough information to let us recreate the objects after
* rewrite.
*/
RememberAllDependentForRebuilding(tab, AT_SetExpression, rel, attnum, colName);
/*
* Drop the dependency records of the GENERATED expression, in particular
* its INTERNAL dependency on the column, which would otherwise cause
* dependency.c to refuse to perform the deletion.
*/
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 above changes visible */
CommandCounterIncrement();
/*
* Get rid of the GENERATED expression itself. We use RESTRICT here for
* safety, but at present we do not expect anything to depend on the
* expression.
*/
RemoveAttrDefault(RelationGetRelid(rel), attnum, DROP_RESTRICT,
false, false);
/* Prepare to store the new expression, in the catalogs */
rawEnt = (RawColumnDefault *) palloc(sizeof(RawColumnDefault));
rawEnt->attnum = attnum;
rawEnt->raw_default = newExpr;
rawEnt->missingMode = false;
rawEnt->generated = ATTRIBUTE_GENERATED_STORED;
/* Store the generated expression */
AddRelationNewConstraints(rel, list_make1(rawEnt), NIL,
false, true, false, NULL);
/* Make above new expression visible */
CommandCounterIncrement();
/* Prepare for table rewrite */
defval = (Expr *) build_column_default(rel, attnum);
newval = (NewColumnValue *) palloc0(sizeof(NewColumnValue));
newval->attnum = attnum;
newval->expr = expression_planner(defval);
newval->is_generated = true;
tab->newvals = lappend(tab->newvals, newval);
tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
/* Drop any pg_statistic entry for the column */
RemoveStatistics(RelationGetRelid(rel), attnum);
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel), attnum);
ObjectAddressSubSet(address, RelationRelationId,
RelationGetRelid(rel), attnum);
return address;
}
/*
* ALTER TABLE ALTER COLUMN DROP EXPRESSION
*/
static void
ATPrepDropExpression(Relation rel, AlterTableCmd *cmd, bool recurse, bool recursing, LOCKMODE lockmode)
{
/*
* Reject ONLY if there are child tables. We could implement this, but it
* is a bit complicated. GENERATED clauses must be attached to the column
* definition and cannot be added later like DEFAULT, so if a child table
* has a generation expression that the parent does not have, the child
* column will necessarily be an attislocal column. So to implement ONLY
* here, we'd need extra code to update attislocal of the direct child
* tables, somewhat similar to how DROP COLUMN does it, so that the
* resulting state can be properly dumped and restored.
*/
if (!recurse &&
find_inheritance_children(RelationGetRelid(rel), lockmode))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("ALTER TABLE / DROP EXPRESSION must be applied to child tables too")));
/*
* Cannot drop generation expression from inherited columns.
*/
if (!recursing)
{
HeapTuple tuple;
Form_pg_attribute attTup;
tuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), cmd->name);
if (!HeapTupleIsValid(tuple))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_COLUMN),
errmsg("column \"%s\" of relation \"%s\" does not exist",
cmd->name, RelationGetRelationName(rel))));
attTup = (Form_pg_attribute) GETSTRUCT(tuple);
if (attTup->attinhcount > 0)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("cannot drop generation expression from inherited column")));
}
}
/*
* Return the address of the affected column.
*/
static ObjectAddress
ATExecDropExpression(Relation rel, const char *colName, bool missing_ok, LOCKMODE lockmode)
{
HeapTuple tuple;
Form_pg_attribute attTup;
AttrNumber attnum;
Relation attrelation;
Oid attrdefoid;
ObjectAddress address;
attrelation = table_open(AttributeRelationId, RowExclusiveLock);
tuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), colName);
if (!HeapTupleIsValid(tuple))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_COLUMN),
errmsg("column \"%s\" of relation \"%s\" does not exist",
colName, RelationGetRelationName(rel))));
attTup = (Form_pg_attribute) GETSTRUCT(tuple);
attnum = attTup->attnum;
if (attnum <= 0)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot alter system column \"%s\"",
colName)));
if (attTup->attgenerated != ATTRIBUTE_GENERATED_STORED)
{
if (!missing_ok)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("column \"%s\" of relation \"%s\" is not a stored generated column",
colName, RelationGetRelationName(rel))));
else
{
ereport(NOTICE,
(errmsg("column \"%s\" of relation \"%s\" is not a stored generated column, skipping",
colName, RelationGetRelationName(rel))));
heap_freetuple(tuple);
table_close(attrelation, RowExclusiveLock);
return InvalidObjectAddress;
}
}
/*
* Mark the column as no longer generated. (The atthasdef flag needs to
* get cleared too, but RemoveAttrDefault will handle that.)
*/
attTup->attgenerated = '\0';
CatalogTupleUpdate(attrelation, &tuple->t_self, tuple);
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel),
attnum);
heap_freetuple(tuple);
table_close(attrelation, RowExclusiveLock);
/*
* Drop the dependency records of the GENERATED expression, in particular
* its INTERNAL dependency on the column, which would otherwise cause
* dependency.c to refuse to perform the deletion.
*/
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 above changes visible */
CommandCounterIncrement();
/*
* Get rid of the GENERATED expression itself. 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,
false, false);
ObjectAddressSubSet(address, RelationRelationId,
RelationGetRelid(rel), attnum);
return address;
}
/*
* ALTER TABLE ALTER COLUMN SET STATISTICS
*
* Return value is the address of the modified column
*/
static ObjectAddress
ATExecSetStatistics(Relation rel, const char *colName, int16 colNum, Node *newValue, LOCKMODE lockmode)
{
int newtarget = 0;
bool newtarget_default;
Relation attrelation;
HeapTuple tuple,
newtuple;
Form_pg_attribute attrtuple;
AttrNumber attnum;
ObjectAddress address;
Datum repl_val[Natts_pg_attribute];
bool repl_null[Natts_pg_attribute];
bool repl_repl[Natts_pg_attribute];
/*
* We allow referencing columns by numbers only for indexes, since table
* column numbers could contain gaps if columns are later dropped.
*/
if (rel->rd_rel->relkind != RELKIND_INDEX &&
rel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX &&
!colName)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot refer to non-index column by number")));
/* -1 was used in previous versions for the default setting */
if (newValue && intVal(newValue) != -1)
{
newtarget = intVal(newValue);
newtarget_default = false;
}
else
newtarget_default = true;
if (!newtarget_default)
{
/*
* Limit target to a sane range
*/
if (newtarget < 0)
{
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("statistics target %d is too low",
newtarget)));
}
else if (newtarget > MAX_STATISTICS_TARGET)
{
newtarget = MAX_STATISTICS_TARGET;
ereport(WARNING,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("lowering statistics target to %d",
newtarget)));
}
}
attrelation = table_open(AttributeRelationId, RowExclusiveLock);
if (colName)
{
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))));
}
else
{
tuple = SearchSysCacheAttNum(RelationGetRelid(rel), colNum);
if (!HeapTupleIsValid(tuple))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_COLUMN),
errmsg("column number %d of relation \"%s\" does not exist",
colNum, RelationGetRelationName(rel))));
}
attrtuple = (Form_pg_attribute) GETSTRUCT(tuple);
attnum = attrtuple->attnum;
if (attnum <= 0)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot alter system column \"%s\"",
colName)));
if (rel->rd_rel->relkind == RELKIND_INDEX ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)
{
if (attnum > rel->rd_index->indnkeyatts)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot alter statistics on included column \"%s\" of index \"%s\"",
NameStr(attrtuple->attname), RelationGetRelationName(rel))));
else if (rel->rd_index->indkey.values[attnum - 1] != 0)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot alter statistics on non-expression column \"%s\" of index \"%s\"",
NameStr(attrtuple->attname), RelationGetRelationName(rel)),
errhint("Alter statistics on table column instead.")));
}
/* Build new tuple. */
memset(repl_null, false, sizeof(repl_null));
memset(repl_repl, false, sizeof(repl_repl));
if (!newtarget_default)
repl_val[Anum_pg_attribute_attstattarget - 1] = newtarget;
else
repl_null[Anum_pg_attribute_attstattarget - 1] = true;
repl_repl[Anum_pg_attribute_attstattarget - 1] = true;
newtuple = heap_modify_tuple(tuple, RelationGetDescr(attrelation),
repl_val, repl_null, repl_repl);
CatalogTupleUpdate(attrelation, &tuple->t_self, newtuple);
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel),
attrtuple->attnum);
ObjectAddressSubSet(address, RelationRelationId,
RelationGetRelid(rel), attnum);
heap_freetuple(newtuple);
ReleaseSysCache(tuple);
table_close(attrelation, RowExclusiveLock);
return address;
}
/*
* Return value is the address of the modified column
*/
static ObjectAddress
ATExecSetOptions(Relation rel, const char *colName, Node *options,
bool isReset, LOCKMODE lockmode)
{
Relation attrelation;
HeapTuple tuple,
newtuple;
Form_pg_attribute attrtuple;
AttrNumber attnum;
Datum datum,
newOptions;
bool isnull;
ObjectAddress address;
Datum repl_val[Natts_pg_attribute];
bool repl_null[Natts_pg_attribute];
bool repl_repl[Natts_pg_attribute];
attrelation = 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))));
attrtuple = (Form_pg_attribute) GETSTRUCT(tuple);
attnum = attrtuple->attnum;
if (attnum <= 0)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot alter system column \"%s\"",
colName)));
/* Generate new proposed attoptions (text array) */
datum = SysCacheGetAttr(ATTNAME, tuple, Anum_pg_attribute_attoptions,
&isnull);
newOptions = transformRelOptions(isnull ? (Datum) 0 : datum,
castNode(List, options), NULL, NULL,
false, isReset);
/* Validate new options */
(void) attribute_reloptions(newOptions, true);
/* Build new tuple. */
memset(repl_null, false, sizeof(repl_null));
memset(repl_repl, false, sizeof(repl_repl));
if (newOptions != (Datum) 0)
repl_val[Anum_pg_attribute_attoptions - 1] = newOptions;
else
repl_null[Anum_pg_attribute_attoptions - 1] = true;
repl_repl[Anum_pg_attribute_attoptions - 1] = true;
newtuple = heap_modify_tuple(tuple, RelationGetDescr(attrelation),
repl_val, repl_null, repl_repl);
/* Update system catalog. */
CatalogTupleUpdate(attrelation, &newtuple->t_self, newtuple);
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel),
attrtuple->attnum);
ObjectAddressSubSet(address, RelationRelationId,
RelationGetRelid(rel), attnum);
heap_freetuple(newtuple);
ReleaseSysCache(tuple);
table_close(attrelation, RowExclusiveLock);
return address;
}
/*
* Helper function for ATExecSetStorage and ATExecSetCompression
*
* Set the attstorage and/or attcompression fields for index columns
* associated with the specified table column.
*/
static void
SetIndexStorageProperties(Relation rel, Relation attrelation,
AttrNumber attnum,
bool setstorage, char newstorage,
bool setcompression, char newcompression,
LOCKMODE lockmode)
{
ListCell *lc;
foreach(lc, RelationGetIndexList(rel))
{
Oid indexoid = lfirst_oid(lc);
Relation indrel;
AttrNumber indattnum = 0;
HeapTuple tuple;
indrel = index_open(indexoid, lockmode);
for (int i = 0; i < indrel->rd_index->indnatts; i++)
{
if (indrel->rd_index->indkey.values[i] == attnum)
{
indattnum = i + 1;
break;
}
}
if (indattnum == 0)
{
index_close(indrel, lockmode);
continue;
}
tuple = SearchSysCacheCopyAttNum(RelationGetRelid(indrel), indattnum);
if (HeapTupleIsValid(tuple))
{
Form_pg_attribute attrtuple = (Form_pg_attribute) GETSTRUCT(tuple);
if (setstorage)
attrtuple->attstorage = newstorage;
if (setcompression)
attrtuple->attcompression = newcompression;
CatalogTupleUpdate(attrelation, &tuple->t_self, tuple);
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel),
attrtuple->attnum);
heap_freetuple(tuple);
}
index_close(indrel, lockmode);
}
}
/*
* ALTER TABLE ALTER COLUMN SET STORAGE
*
* Return value is the address of the modified column
*/
static ObjectAddress
ATExecSetStorage(Relation rel, const char *colName, Node *newValue, LOCKMODE lockmode)
{
Relation attrelation;
HeapTuple tuple;
Form_pg_attribute attrtuple;
AttrNumber attnum;
ObjectAddress address;
attrelation = table_open(AttributeRelationId, RowExclusiveLock);
tuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), colName);
if (!HeapTupleIsValid(tuple))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_COLUMN),
errmsg("column \"%s\" of relation \"%s\" does not exist",
colName, RelationGetRelationName(rel))));
attrtuple = (Form_pg_attribute) GETSTRUCT(tuple);
attnum = attrtuple->attnum;
if (attnum <= 0)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot alter system column \"%s\"",
colName)));
attrtuple->attstorage = GetAttributeStorage(attrtuple->atttypid, strVal(newValue));
CatalogTupleUpdate(attrelation, &tuple->t_self, tuple);
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel),
attrtuple->attnum);
/*
* Apply the change to indexes as well (only for simple index columns,
* matching behavior of index.c ConstructTupleDescriptor()).
*/
SetIndexStorageProperties(rel, attrelation, attnum,
true, attrtuple->attstorage,
false, 0,
lockmode);
heap_freetuple(tuple);
table_close(attrelation, RowExclusiveLock);
ObjectAddressSubSet(address, RelationRelationId,
RelationGetRelid(rel), attnum);
return address;
}
/*
* ALTER TABLE DROP COLUMN
*
* DROP COLUMN cannot use the normal ALTER TABLE recursion mechanism,
* because we have to decide at runtime whether to recurse or not depending
* on whether attinhcount goes to zero or not. (We can't check this in a
* static pre-pass because it won't handle multiple inheritance situations
* correctly.)
*/
static void
ATPrepDropColumn(List **wqueue, Relation rel, bool recurse, bool recursing,
AlterTableCmd *cmd, LOCKMODE lockmode,
AlterTableUtilityContext *context)
{
if (rel->rd_rel->reloftype && !recursing)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot drop column from typed table")));
if (rel->rd_rel->relkind == RELKIND_COMPOSITE_TYPE)
ATTypedTableRecursion(wqueue, rel, cmd, lockmode, context);
if (recurse)
cmd->recurse = true;
}
/*
* Drops column 'colName' from relation 'rel' and returns the address of the
* dropped column. The column is also dropped (or marked as no longer
* inherited from relation) from the relation's inheritance children, if any.
*
* In the recursive invocations for inheritance child relations, instead of
* dropping the column directly (if to be dropped at all), its object address
* is added to 'addrs', which must be non-NULL in such invocations. All
* columns are dropped at the same time after all the children have been
* checked recursively.
*/
static ObjectAddress
ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
DropBehavior behavior,
bool recurse, bool recursing,
bool missing_ok, LOCKMODE lockmode,
ObjectAddresses *addrs)
{
HeapTuple tuple;
Form_pg_attribute targetatt;
AttrNumber attnum;
List *children;
ObjectAddress object;
bool is_expr;
/* At top level, permission check was done in ATPrepCmd, else do it */
if (recursing)
ATSimplePermissions(AT_DropColumn, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
/* Initialize addrs on the first invocation */
Assert(!recursing || addrs != NULL);
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
if (!recursing)
addrs = new_object_addresses();
/*
* get the number of the attribute
*/
tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName);
if (!HeapTupleIsValid(tuple))
{
if (!missing_ok)
{
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_COLUMN),
errmsg("column \"%s\" of relation \"%s\" does not exist",
colName, RelationGetRelationName(rel))));
}
else
{
ereport(NOTICE,
(errmsg("column \"%s\" of relation \"%s\" does not exist, skipping",
colName, RelationGetRelationName(rel))));
return InvalidObjectAddress;
}
}
targetatt = (Form_pg_attribute) GETSTRUCT(tuple);
attnum = targetatt->attnum;
/* Can't drop a system attribute */
if (attnum <= 0)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot drop system column \"%s\"",
colName)));
/*
* Don't drop inherited columns, unless recursing (presumably from a drop
* of the parent column)
*/
if (targetatt->attinhcount > 0 && !recursing)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("cannot drop inherited column \"%s\"",
colName)));
/*
* Don't drop columns used in the partition key, either. (If we let this
* go through, the key column's dependencies would cause a cascaded drop
* of the whole table, which is surely not what the user expected.)
*/
if (has_partition_attrs(rel,
bms_make_singleton(attnum - FirstLowInvalidHeapAttributeNumber),
&is_expr))
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("cannot drop column \"%s\" because it is part of the partition key of relation \"%s\"",
colName, RelationGetRelationName(rel))));
ReleaseSysCache(tuple);
/*
* 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.
*/
children =
find_inheritance_children(RelationGetRelid(rel), lockmode);
if (children)
{
Relation attr_rel;
ListCell *child;
/*
* In case of a partitioned table, the column must be dropped from the
* partitions as well.
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE && !recurse)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("cannot drop column from only the partitioned table when partitions exist"),
errhint("Do not specify the ONLY keyword.")));
attr_rel = table_open(AttributeRelationId, RowExclusiveLock);
foreach(child, children)
{
Oid childrelid = lfirst_oid(child);
Relation childrel;
Form_pg_attribute childatt;
/* find_inheritance_children already got lock */
childrel = table_open(childrelid, NoLock);
CheckTableNotInUse(childrel, "ALTER TABLE");
tuple = SearchSysCacheCopyAttName(childrelid, colName);
if (!HeapTupleIsValid(tuple)) /* shouldn't happen */
elog(ERROR, "cache lookup failed for attribute \"%s\" of relation %u",
colName, childrelid);
childatt = (Form_pg_attribute) GETSTRUCT(tuple);
if (childatt->attinhcount <= 0) /* shouldn't happen */
elog(ERROR, "relation %u has non-inherited attribute \"%s\"",
childrelid, colName);
if (recurse)
{
/*
* If the child column has other definition sources, just
* decrement its inheritance count; if not, recurse to delete
* it.
*/
if (childatt->attinhcount == 1 && !childatt->attislocal)
{
/* Time to delete this child column, too */
ATExecDropColumn(wqueue, childrel, colName,
behavior, true, true,
false, lockmode, addrs);
}
else
{
/* Child column must survive my deletion */
childatt->attinhcount--;
CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
/* Make update visible */
CommandCounterIncrement();
}
}
else
{
/*
* If we were told to drop ONLY in this table (no recursion),
* we need to mark the inheritors' attributes as locally
* defined rather than inherited.
*/
childatt->attinhcount--;
childatt->attislocal = true;
CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
/* Make update visible */
CommandCounterIncrement();
}
heap_freetuple(tuple);
table_close(childrel, NoLock);
}
table_close(attr_rel, RowExclusiveLock);
}
/* Add object to delete */
object.classId = RelationRelationId;
object.objectId = RelationGetRelid(rel);
object.objectSubId = attnum;
add_exact_object_address(&object, addrs);
if (!recursing)
{
/* Recursion has ended, drop everything that was collected */
performMultipleDeletions(addrs, behavior, 0);
free_object_addresses(addrs);
}
return object;
}
/*
* Prepare to add a primary key on an inheritance parent, by adding NOT NULL
* constraint on its children.
*/
static void
ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
LOCKMODE lockmode, AlterTableUtilityContext *context)
{
List *children;
List *newconstrs = NIL;
IndexStmt *indexstmt;
/* No work if not creating a primary key */
if (!IsA(cmd->def, IndexStmt))
return;
indexstmt = castNode(IndexStmt, cmd->def);
if (!indexstmt->primary)
return;
/* No work if no legacy inheritance children are present */
if (rel->rd_rel->relkind != RELKIND_RELATION ||
!rel->rd_rel->relhassubclass)
return;
/*
* Acquire locks all the way down the hierarchy. The recursion to lower
* levels occurs at execution time as necessary, so we don't need to do it
* here, and we don't need the returned list either.
*/
(void) find_all_inheritors(RelationGetRelid(rel), lockmode, NULL);
/*
* Construct the list of constraints that we need to add to each child
* relation.
*/
foreach_node(IndexElem, elem, indexstmt->indexParams)
{
Constraint *nnconstr;
Assert(elem->expr == NULL);
nnconstr = makeNode(Constraint);
nnconstr->contype = CONSTR_NOTNULL;
nnconstr->conname = NULL; /* XXX use PK name? */
nnconstr->inhcount = 1;
nnconstr->deferrable = false;
nnconstr->initdeferred = false;
nnconstr->location = -1;
nnconstr->keys = list_make1(makeString(elem->name));
nnconstr->skip_validation = false;
nnconstr->initially_valid = true;
newconstrs = lappend(newconstrs, nnconstr);
}
/* Finally, add AT subcommands to add each constraint to each child. */
children = find_inheritance_children(RelationGetRelid(rel), NoLock);
foreach_oid(childrelid, children)
{
Relation childrel = table_open(childrelid, NoLock);
AlterTableCmd *newcmd = makeNode(AlterTableCmd);
ListCell *lc2;
newcmd->subtype = AT_AddConstraint;
newcmd->recurse = true;
foreach(lc2, newconstrs)
{
/* ATPrepCmd copies newcmd, so we can scribble on it here */
newcmd->def = lfirst(lc2);
ATPrepCmd(wqueue, childrel, newcmd,
true, false, lockmode, context);
}
table_close(childrel, NoLock);
}
}
/*
* ALTER TABLE ADD INDEX
*
* There is no such command in the grammar, but parse_utilcmd.c converts
* UNIQUE and PRIMARY KEY constraints into AT_AddIndex subcommands. This lets
* us schedule creation of the index at the appropriate time during ALTER.
*
* Return value is the address of the new index.
*/
static ObjectAddress
ATExecAddIndex(AlteredTableInfo *tab, Relation rel,
IndexStmt *stmt, bool is_rebuild, LOCKMODE lockmode)
{
bool check_rights;
bool skip_build;
bool quiet;
ObjectAddress address;
Assert(IsA(stmt, IndexStmt));
Assert(!stmt->concurrent);
/* The IndexStmt has already been through transformIndexStmt */
Assert(stmt->transformed);
/* suppress schema rights check when rebuilding existing index */
check_rights = !is_rebuild;
/* skip index build if phase 3 will do it or we're reusing an old one */
skip_build = tab->rewrite > 0 || RelFileNumberIsValid(stmt->oldNumber);
/* suppress notices when rebuilding existing index */
quiet = is_rebuild;
address = DefineIndex(RelationGetRelid(rel),
stmt,
InvalidOid, /* no predefined OID */
InvalidOid, /* no parent index */
InvalidOid, /* no parent constraint */
-1, /* total_parts unknown */
true, /* is_alter_table */
check_rights,
false, /* check_not_in_use - we did it already */
skip_build,
quiet);
/*
* If TryReuseIndex() stashed a relfilenumber for us, we used it for the
* new index instead of building from scratch. Restore associated fields.
* This may store InvalidSubTransactionId in both fields, in which case
* relcache.c will assume it can rebuild the relcache entry. Hence, do
* this after the CCI that made catalog rows visible to any rebuild. The
* DROP of the old edition of this index will have scheduled the storage
* for deletion at commit, so cancel that pending deletion.
*/
if (RelFileNumberIsValid(stmt->oldNumber))
{
Relation irel = index_open(address.objectId, NoLock);
irel->rd_createSubid = stmt->oldCreateSubid;
irel->rd_firstRelfilelocatorSubid = stmt->oldFirstRelfilelocatorSubid;
RelationPreserveStorage(irel->rd_locator, true);
index_close(irel, NoLock);
}
return address;
}
/*
* ALTER TABLE ADD STATISTICS
*
* This is no such command in the grammar, but we use this internally to add
* AT_ReAddStatistics subcommands to rebuild extended statistics after a table
* column type change.
*/
static ObjectAddress
ATExecAddStatistics(AlteredTableInfo *tab, Relation rel,
CreateStatsStmt *stmt, bool is_rebuild, LOCKMODE lockmode)
{
ObjectAddress address;
Assert(IsA(stmt, CreateStatsStmt));
/* The CreateStatsStmt has already been through transformStatsStmt */
Assert(stmt->transformed);
address = CreateStatistics(stmt);
return address;
}
/*
* ALTER TABLE ADD CONSTRAINT USING INDEX
*
* Returns the address of the new constraint.
*/
static ObjectAddress
ATExecAddIndexConstraint(AlteredTableInfo *tab, Relation rel,
IndexStmt *stmt, LOCKMODE lockmode)
{
Oid index_oid = stmt->indexOid;
Relation indexRel;
char *indexName;
IndexInfo *indexInfo;
char *constraintName;
char constraintType;
ObjectAddress address;
bits16 flags;
Assert(IsA(stmt, IndexStmt));
Assert(OidIsValid(index_oid));
Assert(stmt->isconstraint);
/*
* Doing this on partitioned tables is not a simple feature to implement,
* so let's punt for now.
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("ALTER TABLE / ADD CONSTRAINT USING INDEX is not supported on partitioned tables")));
indexRel = index_open(index_oid, AccessShareLock);
indexName = pstrdup(RelationGetRelationName(indexRel));
indexInfo = BuildIndexInfo(indexRel);
/* this should have been checked at parse time */
if (!indexInfo->ii_Unique)
elog(ERROR, "index \"%s\" is not unique", indexName);
/*
* Determine name to assign to constraint. We require a constraint to
* have the same name as the underlying index; therefore, use the index's
* existing name as the default constraint name, and if the user
* explicitly gives some other name for the constraint, rename the index
* to match.
*/
constraintName = stmt->idxname;
if (constraintName == NULL)
constraintName = indexName;
else if (strcmp(constraintName, indexName) != 0)
{
ereport(NOTICE,
(errmsg("ALTER TABLE / ADD CONSTRAINT USING INDEX will rename index \"%s\" to \"%s\"",
indexName, constraintName)));
RenameRelationInternal(index_oid, constraintName, false, true);
}
/* Extra checks needed if making primary key */
if (stmt->primary)
index_check_primary_key(rel, indexInfo, true, stmt);
/* Note we currently don't support EXCLUSION constraints here */
if (stmt->primary)
constraintType = CONSTRAINT_PRIMARY;
else
constraintType = CONSTRAINT_UNIQUE;
/* Create the catalog entries for the constraint */
flags = INDEX_CONSTR_CREATE_UPDATE_INDEX |
INDEX_CONSTR_CREATE_REMOVE_OLD_DEPS |
(stmt->initdeferred ? INDEX_CONSTR_CREATE_INIT_DEFERRED : 0) |
(stmt->deferrable ? INDEX_CONSTR_CREATE_DEFERRABLE : 0) |
(stmt->primary ? INDEX_CONSTR_CREATE_MARK_AS_PRIMARY : 0);
address = index_constraint_create(rel,
index_oid,
InvalidOid,
indexInfo,
constraintName,
constraintType,
flags,
allowSystemTableMods,
false); /* is_internal */
index_close(indexRel, NoLock);
return address;
}
/*
* ALTER TABLE ADD CONSTRAINT
*
* Return value is the address of the new constraint; if no constraint was
* added, InvalidObjectAddress is returned.
*/
static ObjectAddress
ATExecAddConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
Constraint *newConstraint, bool recurse, bool is_readd,
LOCKMODE lockmode)
{
ObjectAddress address = InvalidObjectAddress;
Assert(IsA(newConstraint, Constraint));
/*
* Currently, we only expect to see CONSTR_CHECK, CONSTR_NOTNULL and
* CONSTR_FOREIGN nodes arriving here (see the preprocessing done in
* parse_utilcmd.c).
*/
switch (newConstraint->contype)
{
case CONSTR_CHECK:
case CONSTR_NOTNULL:
address =
ATAddCheckNNConstraint(wqueue, tab, rel,
newConstraint, recurse, false, is_readd,
lockmode);
break;
case CONSTR_FOREIGN:
/*
* Assign or validate constraint name
*/
if (newConstraint->conname)
{
if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
RelationGetRelid(rel),
newConstraint->conname))
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
errmsg("constraint \"%s\" for relation \"%s\" already exists",
newConstraint->conname,
RelationGetRelationName(rel))));
}
else
newConstraint->conname =
ChooseConstraintName(RelationGetRelationName(rel),
ChooseForeignKeyConstraintNameAddition(newConstraint->fk_attrs),
"fkey",
RelationGetNamespace(rel),
NIL);
address = ATAddForeignKeyConstraint(wqueue, tab, rel,
newConstraint,
recurse, false,
lockmode);
break;
default:
elog(ERROR, "unrecognized constraint type: %d",
(int) newConstraint->contype);
}
return address;
}
/*
* Generate the column-name portion of the constraint name for a new foreign
* key given the list of column names that reference the referenced
* table. This will be passed to ChooseConstraintName along with the parent
* table name and the "fkey" suffix.
*
* We know that less than NAMEDATALEN characters will actually be used, so we
* can truncate the result once we've generated that many.
*
* XXX see also ChooseExtendedStatisticNameAddition and
* ChooseIndexNameAddition.
*/
static char *
ChooseForeignKeyConstraintNameAddition(List *colnames)
{
char buf[NAMEDATALEN * 2];
int buflen = 0;
ListCell *lc;
buf[0] = '\0';
foreach(lc, colnames)
{
const char *name = strVal(lfirst(lc));
if (buflen > 0)
buf[buflen++] = '_'; /* insert _ between names */
/*
* At this point we have buflen <= NAMEDATALEN. name should be less
* than NAMEDATALEN already, but use strlcpy for paranoia.
*/
strlcpy(buf + buflen, name, NAMEDATALEN);
buflen += strlen(buf + buflen);
if (buflen >= NAMEDATALEN)
break;
}
return pstrdup(buf);
}
/*
* Add a check or not-null constraint to a single table and its children.
* Returns the address of the constraint added to the parent relation,
* if one gets added, or InvalidObjectAddress otherwise.
*
* Subroutine for ATExecAddConstraint.
*
* We must recurse to child tables during execution, rather than using
* ALTER TABLE's normal prep-time recursion. The reason is that all the
* constraints *must* be given the same name, else they won't be seen as
* related later. If the user didn't explicitly specify a name, then
* AddRelationNewConstraints would normally assign different names to the
* child constraints. To fix that, we must capture the name assigned at
* the parent table and pass that down.
*/
static ObjectAddress
ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
Constraint *constr, bool recurse, bool recursing,
bool is_readd, LOCKMODE lockmode)
{
List *newcons;
ListCell *lcon;
List *children;
ListCell *child;
ObjectAddress address = InvalidObjectAddress;
/* 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_AddConstraint, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
/*
* Call AddRelationNewConstraints to do the work, making sure it works on
* a copy of the Constraint so transformExpr can't modify the original. It
* returns a list of cooked constraints.
*
* If the constraint ends up getting merged with a pre-existing one, it's
* omitted from the returned list, which is what we want: we do not need
* to do any validation work. That can only happen at child tables,
* though, since we disallow merging at the top level.
*/
newcons = AddRelationNewConstraints(rel, NIL,
list_make1(copyObject(constr)),
recursing || is_readd, /* allow_merge */
!recursing, /* is_local */
is_readd, /* is_internal */
NULL); /* queryString not available
* here */
/* we don't expect more than one constraint here */
Assert(list_length(newcons) <= 1);
/* Add each to-be-validated constraint to Phase 3's queue */
foreach(lcon, newcons)
{
CookedConstraint *ccon = (CookedConstraint *) lfirst(lcon);
if (!ccon->skip_validation && ccon->contype != CONSTR_NOTNULL)
{
NewConstraint *newcon;
newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
newcon->name = ccon->name;
newcon->contype = ccon->contype;
newcon->qual = ccon->expr;
tab->constraints = lappend(tab->constraints, newcon);
}
/* Save the actually assigned name if it was defaulted */
if (constr->conname == NULL)
constr->conname = ccon->name;
/*
* If adding a not-null constraint, set the pg_attribute flag and tell
* phase 3 to verify existing rows, if needed.
*/
if (constr->contype == CONSTR_NOTNULL)
set_attnotnull(wqueue, rel, ccon->attnum,
!ccon->is_no_inherit, lockmode);
ObjectAddressSet(address, ConstraintRelationId, ccon->conoid);
}
/* At this point we must have a locked-down name to use */
Assert(newcons == NIL || constr->conname != NULL);
/* Advance command counter in case same table is visited multiple times */
CommandCounterIncrement();
/*
* If the constraint got merged with an existing constraint, we're done.
* We mustn't recurse to child tables in this case, because they've
* already got the constraint, and visiting them again would lead to an
* incorrect value for coninhcount.
*/
if (newcons == NIL)
return address;
/*
* If adding a NO INHERIT constraint, no need to find our children.
*/
if (constr->is_no_inherit)
return address;
/*
* 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.
*/
children =
find_inheritance_children(RelationGetRelid(rel), lockmode);
/*
* Check if ONLY was specified with ALTER TABLE. If so, allow the
* constraint creation only if there are no children currently. Error out
* otherwise.
*/
if (!recurse && children != NIL)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("constraint must be added to child tables too")));
/*
* The constraint must appear as inherited in children, so create a
* modified constraint object to use.
*/
constr = copyObject(constr);
constr->inhcount = 1;
foreach(child, children)
{
Oid childrelid = lfirst_oid(child);
Relation childrel;
AlteredTableInfo *childtab;
/* find_inheritance_children already got lock */
childrel = table_open(childrelid, NoLock);
CheckTableNotInUse(childrel, "ALTER TABLE");
/* Find or create work queue entry for this table */
childtab = ATGetQueueEntry(wqueue, childrel);
/*
* Recurse to child. XXX if we didn't create a constraint on the
* parent because it already existed, and we do create one on a child,
* should we return that child's constraint ObjectAddress here?
*/
ATAddCheckNNConstraint(wqueue, childtab, childrel,
constr, recurse, true, is_readd, lockmode);
table_close(childrel, NoLock);
}
return address;
}
/*
* Add a foreign-key constraint to a single table; return the new constraint's
* address.
*
* Subroutine for ATExecAddConstraint. Must already hold exclusive
* lock on the rel, and have done appropriate validity checks for it.
* We do permissions checks here, however.
*
* When the referenced or referencing tables (or both) are partitioned,
* multiple pg_constraint rows are required -- one for each partitioned table
* and each partition on each side (fortunately, not one for every combination
* thereof). We also need action triggers on each leaf partition on the
* referenced side, and check triggers on each leaf partition on the
* referencing side.
*/
static ObjectAddress
ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
Constraint *fkconstraint,
bool recurse, bool recursing, LOCKMODE lockmode)
{
Relation pkrel;
int16 pkattnum[INDEX_MAX_KEYS] = {0};
int16 fkattnum[INDEX_MAX_KEYS] = {0};
Oid pktypoid[INDEX_MAX_KEYS] = {0};
Oid fktypoid[INDEX_MAX_KEYS] = {0};
Oid opclasses[INDEX_MAX_KEYS] = {0};
Oid pfeqoperators[INDEX_MAX_KEYS] = {0};
Oid ppeqoperators[INDEX_MAX_KEYS] = {0};
Oid ffeqoperators[INDEX_MAX_KEYS] = {0};
int16 fkdelsetcols[INDEX_MAX_KEYS] = {0};
bool with_period;
bool pk_has_without_overlaps;
int i;
int numfks,
numpks,
numfkdelsetcols;
Oid indexOid;
bool old_check_ok;
ObjectAddress address;
ListCell *old_pfeqop_item = list_head(fkconstraint->old_conpfeqop);
/*
* Grab ShareRowExclusiveLock on the pk table, so that someone doesn't
* delete rows out from under us.
*/
if (OidIsValid(fkconstraint->old_pktable_oid))
pkrel = table_open(fkconstraint->old_pktable_oid, ShareRowExclusiveLock);
else
pkrel = table_openrv(fkconstraint->pktable, ShareRowExclusiveLock);
/*
* Validity checks (permission checks wait till we have the column
* numbers)
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
if (!recurse)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot use ONLY for foreign key on partitioned table \"%s\" referencing relation \"%s\"",
RelationGetRelationName(rel),
RelationGetRelationName(pkrel))));
if (fkconstraint->skip_validation && !fkconstraint->initially_valid)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot add NOT VALID foreign key on partitioned table \"%s\" referencing relation \"%s\"",
RelationGetRelationName(rel),
RelationGetRelationName(pkrel)),
errdetail("This feature is not yet supported on partitioned tables.")));
}
if (pkrel->rd_rel->relkind != RELKIND_RELATION &&
pkrel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("referenced relation \"%s\" is not a table",
RelationGetRelationName(pkrel))));
if (!allowSystemTableMods && IsSystemRelation(pkrel))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied: \"%s\" is a system catalog",
RelationGetRelationName(pkrel))));
/*
* References from permanent or unlogged tables to temp tables, and from
* permanent tables to unlogged tables, are disallowed because the
* referenced data can vanish out from under us. References from temp
* tables to any other table type are also disallowed, because other
* backends might need to run the RI triggers on the perm table, but they
* can't reliably see tuples in the local buffers of other backends.
*/
switch (rel->rd_rel->relpersistence)
{
case RELPERSISTENCE_PERMANENT:
if (!RelationIsPermanent(pkrel))
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("constraints on permanent tables may reference only permanent tables")));
break;
case RELPERSISTENCE_UNLOGGED:
if (!RelationIsPermanent(pkrel)
&& pkrel->rd_rel->relpersistence != RELPERSISTENCE_UNLOGGED)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("constraints on unlogged tables may reference only permanent or unlogged tables")));
break;
case RELPERSISTENCE_TEMP:
if (pkrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("constraints on temporary tables may reference only temporary tables")));
if (!pkrel->rd_islocaltemp || !rel->rd_islocaltemp)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("constraints on temporary tables must involve temporary tables of this session")));
break;
}
/*
* Look up the referencing attributes to make sure they exist, and record
* their attnums and type OIDs.
*/
numfks = transformColumnNameList(RelationGetRelid(rel),
fkconstraint->fk_attrs,
fkattnum, fktypoid);
with_period = fkconstraint->fk_with_period || fkconstraint->pk_with_period;
if (with_period && !fkconstraint->fk_with_period)
ereport(ERROR,
errcode(ERRCODE_INVALID_FOREIGN_KEY),
errmsg("foreign key uses PERIOD on the referenced table but not the referencing table"));
numfkdelsetcols = transformColumnNameList(RelationGetRelid(rel),
fkconstraint->fk_del_set_cols,
fkdelsetcols, NULL);
validateFkOnDeleteSetColumns(numfks, fkattnum,
numfkdelsetcols, fkdelsetcols,
fkconstraint->fk_del_set_cols);
/*
* If the attribute list for the referenced table was omitted, lookup the
* definition of the primary key and use it. Otherwise, validate the
* supplied attribute list. In either case, discover the index OID and
* index opclasses, and the attnums and type OIDs of the attributes.
*/
if (fkconstraint->pk_attrs == NIL)
{
numpks = transformFkeyGetPrimaryKey(pkrel, &indexOid,
&fkconstraint->pk_attrs,
pkattnum, pktypoid,
opclasses, &pk_has_without_overlaps);
/* If the primary key uses WITHOUT OVERLAPS, the fk must use PERIOD */
if (pk_has_without_overlaps && !fkconstraint->fk_with_period)
ereport(ERROR,
errcode(ERRCODE_INVALID_FOREIGN_KEY),
errmsg("foreign key uses PERIOD on the referenced table but not the referencing table"));
}
else
{
numpks = transformColumnNameList(RelationGetRelid(pkrel),
fkconstraint->pk_attrs,
pkattnum, pktypoid);
/* Since we got pk_attrs, one should be a period. */
if (with_period && !fkconstraint->pk_with_period)
ereport(ERROR,
errcode(ERRCODE_INVALID_FOREIGN_KEY),
errmsg("foreign key uses PERIOD on the referencing table but not the referenced table"));
/* Look for an index matching the column list */
indexOid = transformFkeyCheckAttrs(pkrel, numpks, pkattnum,
with_period, opclasses, &pk_has_without_overlaps);
}
/*
* If the referenced primary key has WITHOUT OVERLAPS, the foreign key
* must use PERIOD.
*/
if (pk_has_without_overlaps && !with_period)
ereport(ERROR,
errcode(ERRCODE_INVALID_FOREIGN_KEY),
errmsg("foreign key must use PERIOD when referencing a primary using WITHOUT OVERLAPS"));
/*
* Now we can check permissions.
*/
checkFkeyPermissions(pkrel, pkattnum, numpks);
/*
* Check some things for generated columns.
*/
for (i = 0; i < numfks; i++)
{
char attgenerated = TupleDescAttr(RelationGetDescr(rel), fkattnum[i] - 1)->attgenerated;
if (attgenerated)
{
/*
* Check restrictions on UPDATE/DELETE actions, per SQL standard
*/
if (fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETNULL ||
fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETDEFAULT ||
fkconstraint->fk_upd_action == FKCONSTR_ACTION_CASCADE)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("invalid %s action for foreign key constraint containing generated column",
"ON UPDATE")));
if (fkconstraint->fk_del_action == FKCONSTR_ACTION_SETNULL ||
fkconstraint->fk_del_action == FKCONSTR_ACTION_SETDEFAULT)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("invalid %s action for foreign key constraint containing generated column",
"ON DELETE")));
}
}
/*
* Some actions are currently unsupported for foreign keys using PERIOD.
*/
if (fkconstraint->fk_with_period)
{
if (fkconstraint->fk_upd_action == FKCONSTR_ACTION_CASCADE ||
fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETNULL ||
fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETDEFAULT)
ereport(ERROR,
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("unsupported %s action for foreign key constraint using PERIOD",
"ON UPDATE"));
if (fkconstraint->fk_del_action == FKCONSTR_ACTION_CASCADE ||
fkconstraint->fk_del_action == FKCONSTR_ACTION_SETNULL ||
fkconstraint->fk_del_action == FKCONSTR_ACTION_SETDEFAULT)
ereport(ERROR,
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("unsupported %s action for foreign key constraint using PERIOD",
"ON DELETE"));
}
/*
* Look up the equality operators to use in the constraint.
*
* Note that we have to be careful about the difference between the actual
* PK column type and the opclass' declared input type, which might be
* only binary-compatible with it. The declared opcintype is the right
* thing to probe pg_amop with.
*/
if (numfks != numpks)
ereport(ERROR,
(errcode(ERRCODE_INVALID_FOREIGN_KEY),
errmsg("number of referencing and referenced columns for foreign key disagree")));
/*
* On the strength of a previous constraint, we might avoid scanning
* tables to validate this one. See below.
*/
old_check_ok = (fkconstraint->old_conpfeqop != NIL);
Assert(!old_check_ok || numfks == list_length(fkconstraint->old_conpfeqop));
for (i = 0; i < numpks; i++)
{
Oid pktype = pktypoid[i];
Oid fktype = fktypoid[i];
Oid fktyped;
HeapTuple cla_ht;
Form_pg_opclass cla_tup;
Oid amid;
Oid opfamily;
Oid opcintype;
Oid pfeqop;
Oid ppeqop;
Oid ffeqop;
int16 eqstrategy;
Oid pfeqop_right;
/* We need several fields out of the pg_opclass entry */
cla_ht = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclasses[i]));
if (!HeapTupleIsValid(cla_ht))
elog(ERROR, "cache lookup failed for opclass %u", opclasses[i]);
cla_tup = (Form_pg_opclass) GETSTRUCT(cla_ht);
amid = cla_tup->opcmethod;
opfamily = cla_tup->opcfamily;
opcintype = cla_tup->opcintype;
ReleaseSysCache(cla_ht);
if (with_period)
{
StrategyNumber rtstrategy;
bool for_overlaps = with_period && i == numpks - 1;
/*
* GiST indexes are required to support temporal foreign keys
* because they combine equals and overlaps.
*/
if (amid != GIST_AM_OID)
elog(ERROR, "only GiST indexes are supported for temporal foreign keys");
rtstrategy = for_overlaps ? RTOverlapStrategyNumber : RTEqualStrategyNumber;
/*
* An opclass can use whatever strategy numbers it wants, so we
* ask the opclass what number it actually uses instead of our RT*
* constants.
*/
eqstrategy = GistTranslateStratnum(opclasses[i], rtstrategy);
if (eqstrategy == InvalidStrategy)
{
HeapTuple tuple;
tuple = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclasses[i]));
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for operator class %u", opclasses[i]);
ereport(ERROR,
errcode(ERRCODE_UNDEFINED_OBJECT),
for_overlaps
? errmsg("could not identify an overlaps operator for foreign key")
: errmsg("could not identify an equality operator for foreign key"),
errdetail("Could not translate strategy number %d for operator class \"%s\" for access method \"%s\".",
rtstrategy, NameStr(((Form_pg_opclass) GETSTRUCT(tuple))->opcname), "gist"));
}
}
else
{
/*
* Check it's a btree; currently this can never fail since no
* other index AMs support unique indexes. If we ever did have
* other types of unique indexes, we'd need a way to determine
* which operator strategy number is equality. (We could use
* something like GistTranslateStratnum.)
*/
if (amid != BTREE_AM_OID)
elog(ERROR, "only b-tree indexes are supported for foreign keys");
eqstrategy = BTEqualStrategyNumber;
}
/*
* There had better be a primary equality operator for the index.
* We'll use it for PK = PK comparisons.
*/
ppeqop = get_opfamily_member(opfamily, opcintype, opcintype,
eqstrategy);
if (!OidIsValid(ppeqop))
elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
eqstrategy, opcintype, opcintype, opfamily);
/*
* Are there equality operators that take exactly the FK type? Assume
* we should look through any domain here.
*/
fktyped = getBaseType(fktype);
pfeqop = get_opfamily_member(opfamily, opcintype, fktyped,
eqstrategy);
if (OidIsValid(pfeqop))
{
pfeqop_right = fktyped;
ffeqop = get_opfamily_member(opfamily, fktyped, fktyped,
eqstrategy);
}
else
{
/* keep compiler quiet */
pfeqop_right = InvalidOid;
ffeqop = InvalidOid;
}
if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop)))
{
/*
* Otherwise, look for an implicit cast from the FK type to the
* opcintype, and if found, use the primary equality operator.
* This is a bit tricky because opcintype might be a polymorphic
* type such as ANYARRAY or ANYENUM; so what we have to test is
* whether the two actual column types can be concurrently cast to
* that type. (Otherwise, we'd fail to reject combinations such
* as int[] and point[].)
*/
Oid input_typeids[2];
Oid target_typeids[2];
input_typeids[0] = pktype;
input_typeids[1] = fktype;
target_typeids[0] = opcintype;
target_typeids[1] = opcintype;
if (can_coerce_type(2, input_typeids, target_typeids,
COERCION_IMPLICIT))
{
pfeqop = ffeqop = ppeqop;
pfeqop_right = opcintype;
}
}
if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop)))
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("foreign key constraint \"%s\" cannot be implemented",
fkconstraint->conname),
errdetail("Key columns \"%s\" and \"%s\" "
"are of incompatible types: %s and %s.",
strVal(list_nth(fkconstraint->fk_attrs, i)),
strVal(list_nth(fkconstraint->pk_attrs, i)),
format_type_be(fktype),
format_type_be(pktype))));
if (old_check_ok)
{
/*
* When a pfeqop changes, revalidate the constraint. We could
* permit intra-opfamily changes, but that adds subtle complexity
* without any concrete benefit for core types. We need not
* assess ppeqop or ffeqop, which RI_Initial_Check() does not use.
*/
old_check_ok = (pfeqop == lfirst_oid(old_pfeqop_item));
old_pfeqop_item = lnext(fkconstraint->old_conpfeqop,
old_pfeqop_item);
}
if (old_check_ok)
{
Oid old_fktype;
Oid new_fktype;
CoercionPathType old_pathtype;
CoercionPathType new_pathtype;
Oid old_castfunc;
Oid new_castfunc;
Form_pg_attribute attr = TupleDescAttr(tab->oldDesc,
fkattnum[i] - 1);
/*
* Identify coercion pathways from each of the old and new FK-side
* column types to the right (foreign) operand type of the pfeqop.
* We may assume that pg_constraint.conkey is not changing.
*/
old_fktype = attr->atttypid;
new_fktype = fktype;
old_pathtype = findFkeyCast(pfeqop_right, old_fktype,
&old_castfunc);
new_pathtype = findFkeyCast(pfeqop_right, new_fktype,
&new_castfunc);
/*
* Upon a change to the cast from the FK column to its pfeqop
* operand, revalidate the constraint. For this evaluation, a
* binary coercion cast is equivalent to no cast at all. While
* type implementors should design implicit casts with an eye
* toward consistency of operations like equality, we cannot
* assume here that they have done so.
*
* A function with a polymorphic argument could change behavior
* arbitrarily in response to get_fn_expr_argtype(). Therefore,
* when the cast destination is polymorphic, we only avoid
* revalidation if the input type has not changed at all. Given
* just the core data types and operator classes, this requirement
* prevents no would-be optimizations.
*
* If the cast converts from a base type to a domain thereon, then
* that domain type must be the opcintype of the unique index.
* Necessarily, the primary key column must then be of the domain
* type. Since the constraint was previously valid, all values on
* the foreign side necessarily exist on the primary side and in
* turn conform to the domain. Consequently, we need not treat
* domains specially here.
*
* Since we require that all collations share the same notion of
* equality (which they do, because texteq reduces to bitwise
* equality), we don't compare collation here.
*
* We need not directly consider the PK type. It's necessarily
* binary coercible to the opcintype of the unique index column,
* and ri_triggers.c will only deal with PK datums in terms of
* that opcintype. Changing the opcintype also changes pfeqop.
*/
old_check_ok = (new_pathtype == old_pathtype &&
new_castfunc == old_castfunc &&
(!IsPolymorphicType(pfeqop_right) ||
new_fktype == old_fktype));
}
pfeqoperators[i] = pfeqop;
ppeqoperators[i] = ppeqop;
ffeqoperators[i] = ffeqop;
}
/*
* For FKs with PERIOD we need additional operators to check whether the
* referencing row's range is contained by the aggregated ranges of the
* referenced row(s). For rangetypes and multirangetypes this is
* fk.periodatt <@ range_agg(pk.periodatt). Those are the only types we
* support for now. FKs will look these up at "runtime", but we should
* make sure the lookup works here, even if we don't use the values.
*/
if (with_period)
{
Oid periodoperoid;
Oid aggedperiodoperoid;
FindFKPeriodOpers(opclasses[numpks - 1], &periodoperoid, &aggedperiodoperoid);
}
/*
* Create all the constraint and trigger objects, recursing to partitions
* as necessary. First handle the referenced side.
*/
address = addFkRecurseReferenced(wqueue, fkconstraint, rel, pkrel,
indexOid,
InvalidOid, /* no parent constraint */
numfks,
pkattnum,
fkattnum,
pfeqoperators,
ppeqoperators,
ffeqoperators,
numfkdelsetcols,
fkdelsetcols,
old_check_ok,
InvalidOid, InvalidOid,
with_period);
/* Now handle the referencing side. */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
indexOid,
address.objectId,
numfks,
pkattnum,
fkattnum,
pfeqoperators,
ppeqoperators,
ffeqoperators,
numfkdelsetcols,
fkdelsetcols,
old_check_ok,
lockmode,
InvalidOid, InvalidOid,
with_period);
/*
* Done. Close pk table, but keep lock until we've committed.
*/
table_close(pkrel, NoLock);
return address;
}
/*
* validateFkOnDeleteSetColumns
* Verifies that columns used in ON DELETE SET NULL/DEFAULT (...)
* column lists are valid.
*/
void
validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
int numfksetcols, const int16 *fksetcolsattnums,
List *fksetcols)
{
for (int i = 0; i < numfksetcols; i++)
{
int16 setcol_attnum = fksetcolsattnums[i];
bool seen = false;
for (int j = 0; j < numfks; j++)
{
if (fkattnums[j] == setcol_attnum)
{
seen = true;
break;
}
}
if (!seen)
{
char *col = strVal(list_nth(fksetcols, i));
ereport(ERROR,
(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
errmsg("column \"%s\" referenced in ON DELETE SET action must be part of foreign key", col)));
}
}
}
/*
* addFkRecurseReferenced
* subroutine for ATAddForeignKeyConstraint; recurses on the referenced
* side of the constraint
*
* Create pg_constraint rows for the referenced side of the constraint,
* referencing the parent of the referencing side; also create action triggers
* on leaf partitions. If the table is partitioned, recurse to handle each
* partition.
*
* wqueue is the ALTER TABLE work queue; can be NULL when not running as part
* of an ALTER TABLE sequence.
* fkconstraint is the constraint being added.
* rel is the root referencing relation.
* pkrel is the referenced relation; might be a partition, if recursing.
* indexOid is the OID of the index (on pkrel) implementing this constraint.
* parentConstr is the OID of a parent constraint; InvalidOid if this is a
* top-level constraint.
* numfks is the number of columns in the foreign key
* pkattnum is the attnum array of referenced attributes.
* fkattnum is the attnum array of referencing attributes.
* numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
* (...) clause
* fkdelsetcols is the attnum array of the columns in the ON DELETE SET
* NULL/DEFAULT clause
* pf/pp/ffeqoperators are OID array of operators between columns.
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
* parentDelTrigger and parentUpdTrigger, when being recursively called on
* a partition, are the OIDs of the parent action triggers for DELETE and
* UPDATE respectively.
*/
static ObjectAddress
addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks,
int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
Oid *ppeqoperators, Oid *ffeqoperators,
int numfkdelsetcols, int16 *fkdelsetcols,
bool old_check_ok,
Oid parentDelTrigger, Oid parentUpdTrigger,
bool with_period)
{
ObjectAddress address;
Oid constrOid;
char *conname;
bool conislocal;
int coninhcount;
bool connoinherit;
Oid deleteTriggerOid,
updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
* is redundant with a previous check, but we need it when recursing.
*/
if (pkrel->rd_rel->relkind != RELKIND_RELATION &&
pkrel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("referenced relation \"%s\" is not a table",
RelationGetRelationName(pkrel))));
/*
* Caller supplies us with a constraint name; however, it may be used in
* this partition, so come up with a different one in that case.
*/
if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
RelationGetRelid(rel),
fkconstraint->conname))
conname = ChooseConstraintName(RelationGetRelationName(rel),
ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
"fkey",
RelationGetNamespace(rel), NIL);
else
conname = fkconstraint->conname;
if (OidIsValid(parentConstr))
{
conislocal = false;
coninhcount = 1;
connoinherit = false;
}
else
{
conislocal = true;
coninhcount = 0;
/*
* always inherit for partitioned tables, never for legacy inheritance
*/
connoinherit = rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE;
}
/*
* Record the FK constraint in pg_constraint.
*/
constrOid = CreateConstraintEntry(conname,
RelationGetNamespace(rel),
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
fkconstraint->initially_valid,
parentConstr,
RelationGetRelid(rel),
fkattnum,
numfks,
numfks,
InvalidOid, /* not a domain constraint */
indexOid,
RelationGetRelid(pkrel),
pkattnum,
pfeqoperators,
ppeqoperators,
ffeqoperators,
numfks,
fkconstraint->fk_upd_action,
fkconstraint->fk_del_action,
fkdelsetcols,
numfkdelsetcols,
fkconstraint->fk_matchtype,
NULL, /* no exclusion constraint */
NULL, /* no check constraint */
NULL,
conislocal, /* islocal */
coninhcount, /* inhcount */
connoinherit, /* conNoInherit */
with_period, /* conPeriod */
false); /* is_internal */
ObjectAddressSet(address, ConstraintRelationId, constrOid);
/*
* Mark the child constraint as part of the parent constraint; it must not
* be dropped on its own. (This constraint is deleted when the partition
* is detached, but a special check needs to occur that the partition
* contains no referenced values.)
*/
if (OidIsValid(parentConstr))
{
ObjectAddress referenced;
ObjectAddressSet(referenced, ConstraintRelationId, parentConstr);
recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL);
}
/* make new constraint visible, in case we add more */
CommandCounterIncrement();
/*
* Create the action triggers that enforce the constraint.
*/
createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
fkconstraint,
constrOid, indexOid,
parentDelTrigger, parentUpdTrigger,
&deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
* each partition. We need one pg_constraint row created for each
* partition in addition to the pg_constraint row for the parent table.
*/
if (pkrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
PartitionDesc pd = RelationGetPartitionDesc(pkrel, true);
for (int i = 0; i < pd->nparts; i++)
{
Relation partRel;
AttrMap *map;
AttrNumber *mapped_pkattnum;
Oid partIndexId;
partRel = table_open(pd->oids[i], ShareRowExclusiveLock);
/*
* Map the attribute numbers in the referenced side of the FK
* definition to match the partition's column layout.
*/
map = build_attrmap_by_name_if_req(RelationGetDescr(partRel),
RelationGetDescr(pkrel),
false);
if (map)
{
mapped_pkattnum = palloc(sizeof(AttrNumber) * numfks);
for (int j = 0; j < numfks; j++)
mapped_pkattnum[j] = map->attnums[pkattnum[j] - 1];
}
else
mapped_pkattnum = pkattnum;
/* do the deed */
partIndexId = index_get_partition(partRel, indexOid);
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partRel));
addFkRecurseReferenced(wqueue, fkconstraint, rel, partRel,
partIndexId, constrOid, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
numfkdelsetcols, fkdelsetcols,
old_check_ok,
deleteTriggerOid, updateTriggerOid,
with_period);
/* Done -- clean up (but keep the lock) */
table_close(partRel, NoLock);
if (map)
{
pfree(mapped_pkattnum);
free_attrmap(map);
}
}
}
return address;
}
/*
* addFkRecurseReferencing
* subroutine for ATAddForeignKeyConstraint and CloneFkReferencing
*
* If the referencing relation is a plain relation, create the necessary check
* triggers that implement the constraint, and set up for Phase 3 constraint
* verification. If the referencing relation is a partitioned table, then
* we create a pg_constraint row for it and recurse on this routine for each
* partition.
*
* We assume that the referenced relation is locked against concurrent
* deletions. If it's a partitioned relation, every partition must be so
* locked.
*
* wqueue is the ALTER TABLE work queue; can be NULL when not running as part
* of an ALTER TABLE sequence.
* fkconstraint is the constraint being added.
* rel is the referencing relation; might be a partition, if recursing.
* pkrel is the root referenced relation.
* indexOid is the OID of the index (on pkrel) implementing this constraint.
* parentConstr is the OID of the parent constraint (there is always one).
* numfks is the number of columns in the foreign key
* pkattnum is the attnum array of referenced attributes.
* fkattnum is the attnum array of referencing attributes.
* pf/pp/ffeqoperators are OID array of operators between columns.
* numfkdelsetcols is the number of columns in the ON DELETE SET NULL/DEFAULT
* (...) clause
* fkdelsetcols is the attnum array of the columns in the ON DELETE SET
* NULL/DEFAULT clause
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
* lockmode is the lockmode to acquire on partitions when recursing.
* parentInsTrigger and parentUpdTrigger, when being recursively called on
* a partition, are the OIDs of the parent check triggers for INSERT and
* UPDATE respectively.
*/
static void
addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
int numfkdelsetcols, int16 *fkdelsetcols,
bool old_check_ok, LOCKMODE lockmode,
Oid parentInsTrigger, Oid parentUpdTrigger,
bool with_period)
{
Oid insertTriggerOid,
updateTriggerOid;
Assert(OidIsValid(parentConstr));
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("foreign key constraints are not supported on foreign tables")));
/*
* Add the check triggers to it and, if necessary, schedule it to be
* checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
createForeignKeyCheckTriggers(RelationGetRelid(rel),
RelationGetRelid(pkrel),
fkconstraint,
parentConstr,
indexOid,
parentInsTrigger, parentUpdTrigger,
&insertTriggerOid, &updateTriggerOid);
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
* rows. We can skip this during table creation, when requested
* explicitly by specifying NOT VALID in an ADD FOREIGN KEY command,
* and when we're recreating a constraint following a SET DATA TYPE
* operation that did not impugn its validity.
*/
if (wqueue && !old_check_ok && !fkconstraint->skip_validation)
{
NewConstraint *newcon;
AlteredTableInfo *tab;
tab = ATGetQueueEntry(wqueue, rel);
newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
newcon->name = get_constraint_name(parentConstr);
newcon->contype = CONSTR_FOREIGN;
newcon->refrelid = RelationGetRelid(pkrel);
newcon->refindid = indexOid;
newcon->conid = parentConstr;
newcon->conwithperiod = fkconstraint->fk_with_period;
newcon->qual = (Node *) fkconstraint;
tab->constraints = lappend(tab->constraints, newcon);
}
}
else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
PartitionDesc pd = RelationGetPartitionDesc(rel, true);
Relation trigrel;
/*
* Triggers of the foreign keys will be manipulated a bunch of times
* in the loop below. To avoid repeatedly opening/closing the trigger
* catalog relation, we open it here and pass it to the subroutines
* called below.
*/
trigrel = table_open(TriggerRelationId, RowExclusiveLock);
/*
* Recurse to take appropriate action on each partition; either we
* find an existing constraint to reparent to ours, or we create a new
* one.
*/
for (int i = 0; i < pd->nparts; i++)
{
Oid partitionId = pd->oids[i];
Relation partition = table_open(partitionId, lockmode);
List *partFKs;
AttrMap *attmap;
AttrNumber mapped_fkattnum[INDEX_MAX_KEYS];
bool attached;
char *conname;
Oid constrOid;
ObjectAddress address,
referenced;
ListCell *cell;
CheckTableNotInUse(partition, "ALTER TABLE");
attmap = build_attrmap_by_name(RelationGetDescr(partition),
RelationGetDescr(rel),
false);
for (int j = 0; j < numfks; j++)
mapped_fkattnum[j] = attmap->attnums[fkattnum[j] - 1];
/* Check whether an existing constraint can be repurposed */
partFKs = copyObject(RelationGetFKeyList(partition));
attached = false;
foreach(cell, partFKs)
{
ForeignKeyCacheInfo *fk;
fk = lfirst_node(ForeignKeyCacheInfo, cell);
if (tryAttachPartitionForeignKey(fk,
partitionId,
parentConstr,
numfks,
mapped_fkattnum,
pkattnum,
pfeqoperators,
insertTriggerOid,
updateTriggerOid,
trigrel))
{
attached = true;
break;
}
}
if (attached)
{
table_close(partition, NoLock);
continue;
}
/*
* No luck finding a good constraint to reuse; create our own.
*/
if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
RelationGetRelid(partition),
fkconstraint->conname))
conname = ChooseConstraintName(RelationGetRelationName(partition),
ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
"fkey",
RelationGetNamespace(partition), NIL);
else
conname = fkconstraint->conname;
constrOid =
CreateConstraintEntry(conname,
RelationGetNamespace(partition),
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
fkconstraint->initially_valid,
parentConstr,
partitionId,
mapped_fkattnum,
numfks,
numfks,
InvalidOid,
indexOid,
RelationGetRelid(pkrel),
pkattnum,
pfeqoperators,
ppeqoperators,
ffeqoperators,
numfks,
fkconstraint->fk_upd_action,
fkconstraint->fk_del_action,
fkdelsetcols,
numfkdelsetcols,
fkconstraint->fk_matchtype,
NULL,
NULL,
NULL,
false,
1,
false,
with_period, /* conPeriod */
false);
/*
* Give this constraint partition-type dependencies on the parent
* constraint as well as the table.
*/
ObjectAddressSet(address, ConstraintRelationId, constrOid);
ObjectAddressSet(referenced, ConstraintRelationId, parentConstr);
recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
ObjectAddressSet(referenced, RelationRelationId, partitionId);
recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
/* Make all this visible before recursing */
CommandCounterIncrement();
/* call ourselves to finalize the creation and we're done */
addFkRecurseReferencing(wqueue, fkconstraint, partition, pkrel,
indexOid,
constrOid,
numfks,
pkattnum,
mapped_fkattnum,
pfeqoperators,
ppeqoperators,
ffeqoperators,
numfkdelsetcols,
fkdelsetcols,
old_check_ok,
lockmode,
insertTriggerOid,
updateTriggerOid,
with_period);
table_close(partition, NoLock);
}
table_close(trigrel, RowExclusiveLock);
}
}
/*
* CloneForeignKeyConstraints
* Clone foreign keys from a partitioned table to a newly acquired
* partition.
*
* partitionRel is a partition of parentRel, so we can be certain that it has
* the same columns with the same datatypes. The columns may be in different
* order, though.
*
* wqueue must be passed to set up phase 3 constraint checking, unless the
* referencing-side partition is known to be empty (such as in CREATE TABLE /
* PARTITION OF).
*/
static void
CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel)
{
/* This only works for declarative partitioning */
Assert(parentRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
/*
* Clone constraints for which the parent is on the referenced side.
*/
CloneFkReferenced(parentRel, partitionRel);
/*
* Now clone constraints where the parent is on the referencing side.
*/
CloneFkReferencing(wqueue, parentRel, partitionRel);
}
/*
* CloneFkReferenced
* Subroutine for CloneForeignKeyConstraints
*
* Find all the FKs that have the parent relation on the referenced side;
* clone those constraints to the given partition. This is to be called
* when the partition is being created or attached.
*
* This ignores self-referencing FKs; those are handled by CloneFkReferencing.
*
* This recurses to partitions, if the relation being attached is partitioned.
* Recursion is done by calling addFkRecurseReferenced.
*/
static void
CloneFkReferenced(Relation parentRel, Relation partitionRel)
{
Relation pg_constraint;
AttrMap *attmap;
ListCell *cell;
SysScanDesc scan;
ScanKeyData key[2];
HeapTuple tuple;
List *clone = NIL;
Relation trigrel;
/*
* Search for any constraints where this partition's parent is in the
* referenced side. However, we must not clone any constraint whose
* parent constraint is also going to be cloned, to avoid duplicates. So
* do it in two steps: first construct the list of constraints to clone,
* then go over that list cloning those whose parents are not in the list.
* (We must not rely on the parent being seen first, since the catalog
* scan could return children first.)
*/
pg_constraint = table_open(ConstraintRelationId, RowShareLock);
ScanKeyInit(&key[0],
Anum_pg_constraint_confrelid, BTEqualStrategyNumber,
F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(parentRel)));
ScanKeyInit(&key[1],
Anum_pg_constraint_contype, BTEqualStrategyNumber,
F_CHAREQ, CharGetDatum(CONSTRAINT_FOREIGN));
/* 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)
{
Form_pg_constraint constrForm = (Form_pg_constraint) GETSTRUCT(tuple);
clone = lappend_oid(clone, constrForm->oid);
}
systable_endscan(scan);
table_close(pg_constraint, RowShareLock);
/*
* Triggers of the foreign keys will be manipulated a bunch of times in
* the loop below. To avoid repeatedly opening/closing the trigger
* catalog relation, we open it here and pass it to the subroutines called
* below.
*/
trigrel = table_open(TriggerRelationId, RowExclusiveLock);
attmap = build_attrmap_by_name(RelationGetDescr(partitionRel),
RelationGetDescr(parentRel),
false);
foreach(cell, clone)
{
Oid constrOid = lfirst_oid(cell);
Form_pg_constraint constrForm;
Relation fkRel;
Oid indexOid;
Oid partIndexId;
int numfks;
AttrNumber conkey[INDEX_MAX_KEYS];
AttrNumber mapped_confkey[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];
Constraint *fkconstraint;
Oid deleteTriggerOid,
updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for constraint %u", constrOid);
constrForm = (Form_pg_constraint) GETSTRUCT(tuple);
/*
* As explained above: don't try to clone a constraint for which we're
* going to clone the parent.
*/
if (list_member_oid(clone, constrForm->conparentid))
{
ReleaseSysCache(tuple);
continue;
}
/*
* Don't clone self-referencing foreign keys, which can be in the
* partitioned table or in the partition-to-be.
*/
if (constrForm->conrelid == RelationGetRelid(parentRel) ||
constrForm->conrelid == RelationGetRelid(partitionRel))
{
ReleaseSysCache(tuple);
continue;
}
/*
* Because we're only expanding the key space at the referenced side,
* we don't need to prevent any operation in the referencing table, so
* AccessShareLock suffices (assumes that dropping the constraint
* acquires AEL).
*/
fkRel = table_open(constrForm->conrelid, AccessShareLock);
indexOid = constrForm->conindid;
DeconstructFkConstraintRow(tuple,
&numfks,
conkey,
confkey,
conpfeqop,
conppeqop,
conffeqop,
&numfkdelsetcols,
confdelsetcols);
for (int i = 0; i < numfks; i++)
mapped_confkey[i] = attmap->attnums[confkey[i] - 1];
fkconstraint = makeNode(Constraint);
fkconstraint->contype = CONSTRAINT_FOREIGN;
fkconstraint->conname = NameStr(constrForm->conname);
fkconstraint->deferrable = constrForm->condeferrable;
fkconstraint->initdeferred = constrForm->condeferred;
fkconstraint->location = -1;
fkconstraint->pktable = NULL;
/* ->fk_attrs determined below */
fkconstraint->pk_attrs = NIL;
fkconstraint->fk_matchtype = constrForm->confmatchtype;
fkconstraint->fk_upd_action = constrForm->confupdtype;
fkconstraint->fk_del_action = constrForm->confdeltype;
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = true;
/* set up colnames that are used to generate the constraint name */
for (int i = 0; i < numfks; i++)
{
Form_pg_attribute att;
att = TupleDescAttr(RelationGetDescr(fkRel),
conkey[i] - 1);
fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
makeString(NameStr(att->attname)));
}
/*
* Add the new foreign key constraint pointing to the new partition.
* Because this new partition appears in the referenced side of the
* constraint, we don't need to set up for Phase 3 check.
*/
partIndexId = index_get_partition(partitionRel, indexOid);
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partitionRel));
/*
* Get the "action" triggers belonging to the constraint to pass as
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferenced().
*/
GetForeignKeyActionTriggers(trigrel, constrOid,
constrForm->confrelid, constrForm->conrelid,
&deleteTriggerOid, &updateTriggerOid);
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
partitionRel,
partIndexId,
constrOid,
numfks,
mapped_confkey,
conkey,
conpfeqop,
conppeqop,
conffeqop,
numfkdelsetcols,
confdelsetcols,
true,
deleteTriggerOid,
updateTriggerOid,
constrForm->conperiod);
table_close(fkRel, NoLock);
ReleaseSysCache(tuple);
}
table_close(trigrel, RowExclusiveLock);
}
/*
* CloneFkReferencing
* Subroutine for CloneForeignKeyConstraints
*
* For each FK constraint of the parent relation in the given list, find an
* equivalent constraint in its partition relation that can be reparented;
* if one cannot be found, create a new constraint in the partition as its
* child.
*
* If wqueue is given, it is used to set up phase-3 verification for each
* cloned constraint; if omitted, we assume that such verification is not
* needed (example: the partition is being created anew).
*/
static void
CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
{
AttrMap *attmap;
List *partFKs;
List *clone = NIL;
ListCell *cell;
Relation trigrel;
/* obtain a list of constraints that we need to clone */
foreach(cell, RelationGetFKeyList(parentRel))
{
ForeignKeyCacheInfo *fk = lfirst(cell);
clone = lappend_oid(clone, fk->conoid);
}
/*
* Silently do nothing if there's nothing to do. In particular, this
* avoids throwing a spurious error for foreign tables.
*/
if (clone == NIL)
return;
if (partRel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("foreign key constraints are not supported on foreign tables")));
/*
* Triggers of the foreign keys will be manipulated a bunch of times in
* the loop below. To avoid repeatedly opening/closing the trigger
* catalog relation, we open it here and pass it to the subroutines called
* below.
*/
trigrel = table_open(TriggerRelationId, RowExclusiveLock);
/*
* The constraint key may differ, if the columns in the partition are
* different. This map is used to convert them.
*/
attmap = build_attrmap_by_name(RelationGetDescr(partRel),
RelationGetDescr(parentRel),
false);
partFKs = copyObject(RelationGetFKeyList(partRel));
foreach(cell, clone)
{
Oid parentConstrOid = lfirst_oid(cell);
Form_pg_constraint constrForm;
Relation pkrel;
HeapTuple tuple;
int numfks;
AttrNumber conkey[INDEX_MAX_KEYS];
AttrNumber mapped_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];
Constraint *fkconstraint;
bool attached;
Oid indexOid;
Oid constrOid;
ObjectAddress address,
referenced;
ListCell *lc;
Oid insertTriggerOid,
updateTriggerOid;
bool with_period;
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for constraint %u",
parentConstrOid);
constrForm = (Form_pg_constraint) GETSTRUCT(tuple);
/* Don't clone constraints whose parents are being cloned */
if (list_member_oid(clone, constrForm->conparentid))
{
ReleaseSysCache(tuple);
continue;
}
/*
* Need to prevent concurrent deletions. If pkrel is a partitioned
* relation, that means to lock all partitions.
*/
pkrel = table_open(constrForm->confrelid, ShareRowExclusiveLock);
if (pkrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
(void) find_all_inheritors(RelationGetRelid(pkrel),
ShareRowExclusiveLock, NULL);
DeconstructFkConstraintRow(tuple, &numfks, conkey, confkey,
conpfeqop, conppeqop, conffeqop,
&numfkdelsetcols, confdelsetcols);
for (int i = 0; i < numfks; i++)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
/*
* Get the "check" triggers belonging to the constraint to pass as
* parent OIDs for similar triggers that will be created on the
* partition in addFkRecurseReferencing(). They are also passed to
* tryAttachPartitionForeignKey() below to simply assign as parents to
* the partition's existing "check" triggers, that is, if the
* corresponding constraints is deemed attachable to the parent
* constraint.
*/
GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
constrForm->confrelid, constrForm->conrelid,
&insertTriggerOid, &updateTriggerOid);
/*
* Before creating a new constraint, see whether any existing FKs are
* fit for the purpose. If one is, attach the parent constraint to
* it, and don't clone anything. This way we avoid the expensive
* verification step and don't end up with a duplicate FK, and we
* don't need to recurse to partitions for this constraint.
*/
attached = false;
foreach(lc, partFKs)
{
ForeignKeyCacheInfo *fk = lfirst_node(ForeignKeyCacheInfo, lc);
if (tryAttachPartitionForeignKey(fk,
RelationGetRelid(partRel),
parentConstrOid,
numfks,
mapped_conkey,
confkey,
conpfeqop,
insertTriggerOid,
updateTriggerOid,
trigrel))
{
attached = true;
table_close(pkrel, NoLock);
break;
}
}
if (attached)
{
ReleaseSysCache(tuple);
continue;
}
/* No dice. Set up to create our own constraint */
fkconstraint = makeNode(Constraint);
fkconstraint->contype = CONSTRAINT_FOREIGN;
/* ->conname determined below */
fkconstraint->deferrable = constrForm->condeferrable;
fkconstraint->initdeferred = constrForm->condeferred;
fkconstraint->location = -1;
fkconstraint->pktable = NULL;
/* ->fk_attrs determined below */
fkconstraint->pk_attrs = NIL;
fkconstraint->fk_matchtype = constrForm->confmatchtype;
fkconstraint->fk_upd_action = constrForm->confupdtype;
fkconstraint->fk_del_action = constrForm->confdeltype;
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = true;
for (int i = 0; i < numfks; i++)
{
Form_pg_attribute att;
att = TupleDescAttr(RelationGetDescr(partRel),
mapped_conkey[i] - 1);
fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
makeString(NameStr(att->attname)));
}
if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
RelationGetRelid(partRel),
NameStr(constrForm->conname)))
fkconstraint->conname =
ChooseConstraintName(RelationGetRelationName(partRel),
ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
"fkey",
RelationGetNamespace(partRel), NIL);
else
fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
indexOid = constrForm->conindid;
with_period = constrForm->conperiod;
constrOid =
CreateConstraintEntry(fkconstraint->conname,
constrForm->connamespace,
CONSTRAINT_FOREIGN,
fkconstraint->deferrable,
fkconstraint->initdeferred,
constrForm->convalidated,
parentConstrOid,
RelationGetRelid(partRel),
mapped_conkey,
numfks,
numfks,
InvalidOid, /* not a domain constraint */
indexOid,
constrForm->confrelid, /* same foreign rel */
confkey,
conpfeqop,
conppeqop,
conffeqop,
numfks,
fkconstraint->fk_upd_action,
fkconstraint->fk_del_action,
confdelsetcols,
numfkdelsetcols,
fkconstraint->fk_matchtype,
NULL,
NULL,
NULL,
false, /* islocal */
1, /* inhcount */
false, /* conNoInherit */
with_period, /* conPeriod */
true);
/* Set up partition dependencies for the new constraint */
ObjectAddressSet(address, ConstraintRelationId, constrOid);
ObjectAddressSet(referenced, ConstraintRelationId, parentConstrOid);
recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI);
ObjectAddressSet(referenced, RelationRelationId,
RelationGetRelid(partRel));
recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC);
/* Done with the cloned constraint's tuple */
ReleaseSysCache(tuple);
/* Make all this visible before recursing */
CommandCounterIncrement();
addFkRecurseReferencing(wqueue,
fkconstraint,
partRel,
pkrel,
indexOid,
constrOid,
numfks,
confkey,
mapped_conkey,
conpfeqop,
conppeqop,
conffeqop,
numfkdelsetcols,
confdelsetcols,
false, /* no old check exists */
AccessExclusiveLock,
insertTriggerOid,
updateTriggerOid,
with_period);
table_close(pkrel, NoLock);
}
table_close(trigrel, RowExclusiveLock);
}
/*
* When the parent of a partition receives [the referencing side of] a foreign
* key, we must propagate that foreign key to the partition. However, the
* partition might already have an equivalent foreign key; this routine
* compares the given ForeignKeyCacheInfo (in the partition) to the FK defined
* by the other parameters. If they are equivalent, create the link between
* the two constraints and return true.
*
* If the given FK does not match the one defined by rest of the params,
* return false.
*/
static bool
tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
Oid partRelid,
Oid parentConstrOid,
int numfks,
AttrNumber *mapped_conkey,
AttrNumber *confkey,
Oid *conpfeqop,
Oid parentInsTrigger,
Oid parentUpdTrigger,
Relation trigrel)
{
HeapTuple parentConstrTup;
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
Oid insertTriggerOid,
updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
if (!HeapTupleIsValid(parentConstrTup))
elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
/*
* Do some quick & easy initial checks. If any of these fail, we cannot
* use this constraint.
*/
if (fk->confrelid != parentConstr->confrelid || fk->nkeys != numfks)
{
ReleaseSysCache(parentConstrTup);
return false;
}
for (int i = 0; i < numfks; i++)
{
if (fk->conkey[i] != mapped_conkey[i] ||
fk->confkey[i] != confkey[i] ||
fk->conpfeqop[i] != conpfeqop[i])
{
ReleaseSysCache(parentConstrTup);
return false;
}
}
/*
* Looks good so far; do some more extensive checks. Presumably the check
* for 'convalidated' could be dropped, since we don't really care about
* that, but let's be careful for now.
*/
partcontup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(partcontup))
elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
if (OidIsValid(partConstr->conparentid) ||
!partConstr->convalidated ||
partConstr->condeferrable != parentConstr->condeferrable ||
partConstr->condeferred != parentConstr->condeferred ||
partConstr->confupdtype != parentConstr->confupdtype ||
partConstr->confdeltype != parentConstr->confdeltype ||
partConstr->confmatchtype != parentConstr->confmatchtype)
{
ReleaseSysCache(parentConstrTup);
ReleaseSysCache(partcontup);
return false;
}
ReleaseSysCache(partcontup);
ReleaseSysCache(parentConstrTup);
/*
* Looks good! Attach this constraint. The action triggers in the new
* partition become redundant -- the parent table already has equivalent
* ones, and those will be able to reach the partition. Remove the ones
* in the partition. We identify them because they have our constraint
* OID, as well as being on the referenced rel.
*/
ScanKeyInit(&key,
Anum_pg_trigger_tgconstraint,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(fk->conoid));
scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
NULL, 1, &key);
while ((trigtup = systable_getnext(scan)) != NULL)
{
Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
ObjectAddress trigger;
if (trgform->tgconstrrelid != fk->conrelid)
continue;
if (trgform->tgrelid != fk->confrelid)
continue;
/*
* The constraint is originally set up to contain this trigger as an
* implementation object, so there's a dependency record that links
* the two; however, since the trigger is no longer needed, we remove
* the dependency link in order to be able to drop the trigger while
* keeping the constraint intact.
*/
deleteDependencyRecordsFor(TriggerRelationId,
trgform->oid,
false);
/* make dependency deletion visible to performDeletion */
CommandCounterIncrement();
ObjectAddressSet(trigger, TriggerRelationId,
trgform->oid);
performDeletion(&trigger, DROP_RESTRICT, 0);
/* make trigger drop visible, in case the loop iterates */
CommandCounterIncrement();
}
systable_endscan(scan);
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
/*
* Like the constraint, attach partition's "check" triggers to the
* corresponding parent triggers.
*/
GetForeignKeyCheckTriggers(trigrel,
fk->conoid, fk->confrelid, fk->conrelid,
&insertTriggerOid, &updateTriggerOid);
Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
partRelid);
Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
partRelid);
CommandCounterIncrement();
return true;
}
/*
* GetForeignKeyActionTriggers
* Returns delete and update "action" triggers of the given relation
* belonging to the given constraint
*/
static void
GetForeignKeyActionTriggers(Relation trigrel,
Oid conoid, Oid confrelid, Oid conrelid,
Oid *deleteTriggerOid,
Oid *updateTriggerOid)
{
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
*deleteTriggerOid = *updateTriggerOid = InvalidOid;
ScanKeyInit(&key,
Anum_pg_trigger_tgconstraint,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(conoid));
scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
NULL, 1, &key);
while ((trigtup = systable_getnext(scan)) != NULL)
{
Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
if (trgform->tgconstrrelid != conrelid)
continue;
if (trgform->tgrelid != confrelid)
continue;
/* Only ever look at "action" triggers on the PK side. */
if (RI_FKey_trigger_type(trgform->tgfoid) != RI_TRIGGER_PK)
continue;
if (TRIGGER_FOR_DELETE(trgform->tgtype))
{
Assert(*deleteTriggerOid == InvalidOid);
*deleteTriggerOid = trgform->oid;
}
else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
{
Assert(*updateTriggerOid == InvalidOid);
*updateTriggerOid = trgform->oid;
}
#ifndef USE_ASSERT_CHECKING
/* In an assert-enabled build, continue looking to find duplicates */
if (OidIsValid(*deleteTriggerOid) && OidIsValid(*updateTriggerOid))
break;
#endif
}
if (!OidIsValid(*deleteTriggerOid))
elog(ERROR, "could not find ON DELETE action trigger of foreign key constraint %u",
conoid);
if (!OidIsValid(*updateTriggerOid))
elog(ERROR, "could not find ON UPDATE action trigger of foreign key constraint %u",
conoid);
systable_endscan(scan);
}
/*
* GetForeignKeyCheckTriggers
* Returns insert and update "check" triggers of the given relation
* belonging to the given constraint
*/
static void
GetForeignKeyCheckTriggers(Relation trigrel,
Oid conoid, Oid confrelid, Oid conrelid,
Oid *insertTriggerOid,
Oid *updateTriggerOid)
{
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
*insertTriggerOid = *updateTriggerOid = InvalidOid;
ScanKeyInit(&key,
Anum_pg_trigger_tgconstraint,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(conoid));
scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
NULL, 1, &key);
while ((trigtup = systable_getnext(scan)) != NULL)
{
Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
if (trgform->tgconstrrelid != confrelid)
continue;
if (trgform->tgrelid != conrelid)
continue;
/* Only ever look at "check" triggers on the FK side. */
if (RI_FKey_trigger_type(trgform->tgfoid) != RI_TRIGGER_FK)
continue;
if (TRIGGER_FOR_INSERT(trgform->tgtype))
{
Assert(*insertTriggerOid == InvalidOid);
*insertTriggerOid = trgform->oid;
}
else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
{
Assert(*updateTriggerOid == InvalidOid);
*updateTriggerOid = trgform->oid;
}
#ifndef USE_ASSERT_CHECKING
/* In an assert-enabled build, continue looking to find duplicates. */
if (OidIsValid(*insertTriggerOid) && OidIsValid(*updateTriggerOid))
break;
#endif
}
if (!OidIsValid(*insertTriggerOid))
elog(ERROR, "could not find ON INSERT check triggers of foreign key constraint %u",
conoid);
if (!OidIsValid(*updateTriggerOid))
elog(ERROR, "could not find ON UPDATE check triggers of foreign key constraint %u",
conoid);
systable_endscan(scan);
}
/*
* ALTER TABLE ALTER CONSTRAINT
*
* Update the attributes of a constraint.
*
* Currently only works for Foreign Key constraints.
*
* If the constraint is modified, returns its address; otherwise, return
* InvalidObjectAddress.
*/
static ObjectAddress
ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, bool recurse,
bool recursing, LOCKMODE lockmode)
{
Constraint *cmdcon;
Relation conrel;
Relation tgrel;
SysScanDesc scan;
ScanKeyData skey[3];
HeapTuple contuple;
Form_pg_constraint currcon;
ObjectAddress address;
List *otherrelids = NIL;
ListCell *lc;
cmdcon = castNode(Constraint, cmd->def);
conrel = table_open(ConstraintRelationId, RowExclusiveLock);
tgrel = table_open(TriggerRelationId, RowExclusiveLock);
/*
* Find and check the target constraint
*/
ScanKeyInit(&skey[0],
Anum_pg_constraint_conrelid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(RelationGetRelid(rel)));
ScanKeyInit(&skey[1],
Anum_pg_constraint_contypid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(InvalidOid));
ScanKeyInit(&skey[2],
Anum_pg_constraint_conname,
BTEqualStrategyNumber, F_NAMEEQ,
CStringGetDatum(cmdcon->conname));
scan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId,
true, NULL, 3, skey);
/* There can be at most one matching row */
if (!HeapTupleIsValid(contuple = systable_getnext(scan)))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("constraint \"%s\" of relation \"%s\" does not exist",
cmdcon->conname, RelationGetRelationName(rel))));
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
if (currcon->contype != CONSTRAINT_FOREIGN)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
cmdcon->conname, RelationGetRelationName(rel))));
/*
* If it's not the topmost constraint, raise an error.
*
* Altering a non-topmost constraint leaves some triggers untouched, since
* they are not directly connected to this constraint; also, pg_dump would
* ignore the deferrability status of the individual constraint, since it
* only dumps topmost constraints. Avoid these problems by refusing this
* operation and telling the user to alter the parent constraint instead.
*/
if (OidIsValid(currcon->conparentid))
{
HeapTuple tp;
Oid parent = currcon->conparentid;
char *ancestorname = NULL;
char *ancestortable = NULL;
/* Loop to find the topmost constraint */
while (HeapTupleIsValid(tp = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parent))))
{
Form_pg_constraint contup = (Form_pg_constraint) GETSTRUCT(tp);
/* If no parent, this is the constraint we want */
if (!OidIsValid(contup->conparentid))
{
ancestorname = pstrdup(NameStr(contup->conname));
ancestortable = get_rel_name(contup->conrelid);
ReleaseSysCache(tp);
break;
}
parent = contup->conparentid;
ReleaseSysCache(tp);
}
ereport(ERROR,
(errmsg("cannot alter constraint \"%s\" on relation \"%s\"",
cmdcon->conname, RelationGetRelationName(rel)),
ancestorname && ancestortable ?
errdetail("Constraint \"%s\" is derived from constraint \"%s\" of relation \"%s\".",
cmdcon->conname, ancestorname, ancestortable) : 0,
errhint("You may alter the constraint it derives from instead.")));
}
/*
* Do the actual catalog work. We can skip changing if already in the
* desired state, but not if a partitioned table: partitions need to be
* processed regardless, in case they had the constraint locally changed.
*/
address = InvalidObjectAddress;
if (currcon->condeferrable != cmdcon->deferrable ||
currcon->condeferred != cmdcon->initdeferred ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
if (ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, rel, contuple,
&otherrelids, lockmode))
ObjectAddressSet(address, ConstraintRelationId, currcon->oid);
}
/*
* ATExecAlterConstrRecurse already invalidated relcache for the relations
* having the constraint itself; here we also invalidate for relations
* that have any triggers that are part of the constraint.
*/
foreach(lc, otherrelids)
CacheInvalidateRelcacheByRelid(lfirst_oid(lc));
systable_endscan(scan);
table_close(tgrel, RowExclusiveLock);
table_close(conrel, RowExclusiveLock);
return address;
}
/*
* Recursive subroutine of ATExecAlterConstraint. Returns true if the
* constraint 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
ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
Relation rel, HeapTuple contuple, List **otherrelids,
LOCKMODE lockmode)
{
Form_pg_constraint currcon;
Oid conoid;
Oid refrelid;
bool changed = false;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
currcon = (Form_pg_constraint) GETSTRUCT(contuple);
conoid = currcon->oid;
refrelid = currcon->confrelid;
/*
* Update pg_constraint with the flags from cmdcon.
*
* If called to modify a constraint that's already in the desired state,
* silently do nothing.
*/
if (currcon->condeferrable != cmdcon->deferrable ||
currcon->condeferred != cmdcon->initdeferred)
{
HeapTuple copyTuple;
Form_pg_constraint copy_con;
HeapTuple tgtuple;
ScanKeyData tgkey;
SysScanDesc tgscan;
copyTuple = heap_copytuple(contuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
copy_con->condeferrable = cmdcon->deferrable;
copy_con->condeferred = cmdcon->initdeferred;
CatalogTupleUpdate(conrel, &copyTuple->t_self, copyTuple);
InvokeObjectPostAlterHook(ConstraintRelationId,
conoid, 0);
heap_freetuple(copyTuple);
changed = true;
/* Make new constraint flags visible to others */
CacheInvalidateRelcache(rel);
/*
* Now we need to update the multiple entries in pg_trigger that
* implement the constraint.
*/
ScanKeyInit(&tgkey,
Anum_pg_trigger_tgconstraint,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(conoid));
tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true,
NULL, 1, &tgkey);
while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan)))
{
Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tgtuple);
Form_pg_trigger copy_tg;
HeapTuple tgCopyTuple;
/*
* Remember OIDs of other relation(s) involved in FK constraint.
* (Note: it's likely that we could skip forcing a relcache inval
* for other rels that don't have a trigger whose properties
* change, but let's be conservative.)
*/
if (tgform->tgrelid != RelationGetRelid(rel))
*otherrelids = list_append_unique_oid(*otherrelids,
tgform->tgrelid);
/*
* Update deferrability of RI_FKey_noaction_del,
* RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd
* triggers, but not others; see createForeignKeyActionTriggers
* and CreateFKCheckTrigger.
*/
if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
continue;
tgCopyTuple = heap_copytuple(tgtuple);
copy_tg = (Form_pg_trigger) GETSTRUCT(tgCopyTuple);
copy_tg->tgdeferrable = cmdcon->deferrable;
copy_tg->tginitdeferred = cmdcon->initdeferred;
CatalogTupleUpdate(tgrel, &tgCopyTuple->t_self, tgCopyTuple);
InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0);
heap_freetuple(tgCopyTuple);
}
systable_endscan(tgscan);
}
/*
* If the table at either end of the constraint is partitioned, we need to
* recurse and handle every constraint that is a child of this one.
*
* (This assumes that the recurse flag is forcibly set for partitioned
* tables, and not set for legacy inheritance, though we don't check for
* that here.)
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
get_rel_relkind(refrelid) == RELKIND_PARTITIONED_TABLE)
{
ScanKeyData pkey;
SysScanDesc pscan;
HeapTuple childtup;
ScanKeyInit(&pkey,
Anum_pg_constraint_conparentid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(conoid));
pscan = systable_beginscan(conrel, ConstraintParentIndexId,
true, NULL, 1, &pkey);
while (HeapTupleIsValid(childtup = systable_getnext(pscan)))
{
Form_pg_constraint childcon = (Form_pg_constraint) GETSTRUCT(childtup);
Relation childrel;
childrel = table_open(childcon->conrelid, lockmode);
ATExecAlterConstrRecurse(cmdcon, conrel, tgrel, childrel, childtup,
otherrelids, lockmode);
table_close(childrel, NoLock);
}
systable_endscan(pscan);
}
return changed;
}
/*
* ALTER TABLE VALIDATE CONSTRAINT
*
* XXX The reason we handle recursion here rather than at Phase 1 is because
* there's no good way to skip recursing when handling foreign keys: there is
* no need to lock children in that case, yet we wouldn't be able to avoid
* doing so at that level.
*
* Return value is the address of the validated constraint. If the constraint
* was already validated, InvalidObjectAddress is returned.
*/
static ObjectAddress
ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode)
{
Relation conrel;
SysScanDesc scan;
ScanKeyData skey[3];
HeapTuple tuple;
Form_pg_constraint con;
ObjectAddress address;
conrel = table_open(ConstraintRelationId, RowExclusiveLock);
/*
* Find and check the target constraint
*/
ScanKeyInit(&skey[0],
Anum_pg_constraint_conrelid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(RelationGetRelid(rel)));
ScanKeyInit(&skey[1],
Anum_pg_constraint_contypid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(InvalidOid));
ScanKeyInit(&skey[2],
Anum_pg_constraint_conname,
BTEqualStrategyNumber, F_NAMEEQ,
CStringGetDatum(constrName));
scan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId,
true, NULL, 3, skey);
/* There can be at most one matching row */
if (!HeapTupleIsValid(tuple = systable_getnext(scan)))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("constraint \"%s\" of relation \"%s\" does not exist",
constrName, RelationGetRelationName(rel))));
con = (Form_pg_constraint) GETSTRUCT(tuple);
if (con->contype != CONSTRAINT_FOREIGN &&
con->contype != CONSTRAINT_CHECK)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key or check constraint",
constrName, RelationGetRelationName(rel))));
if (!con->convalidated)
{
AlteredTableInfo *tab;
HeapTuple copyTuple;
Form_pg_constraint copy_con;
if (con->contype == CONSTRAINT_FOREIGN)
{
NewConstraint *newcon;
Constraint *fkconstraint;
/* Queue validation for phase 3 */
fkconstraint = makeNode(Constraint);
/* for now this is all we need */
fkconstraint->conname = constrName;
newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
newcon->name = constrName;
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, rel);
tab->constraints = lappend(tab->constraints, newcon);
/*
* We disallow creating invalid foreign keys to or from
* partitioned tables, so ignoring the recursion bit is okay.
*/
}
else if (con->contype == CONSTRAINT_CHECK)
{
List *children = NIL;
ListCell *child;
NewConstraint *newcon;
Datum val;
char *conbin;
/*
* If we're recursing, the parent has already done this, so skip
* it. Also, if the constraint is a NO INHERIT constraint, we
* shouldn't try to look for it in the children.
*/
if (!recursing && !con->connoinherit)
children = find_all_inheritors(RelationGetRelid(rel),
lockmode, NULL);
/*
* For CHECK constraints, we must ensure that we only mark the
* constraint as validated on the parent if it's already validated
* on the children.
*
* We recurse before validating on the parent, to reduce risk of
* deadlocks.
*/
foreach(child, children)
{
Oid childoid = lfirst_oid(child);
Relation childrel;
if (childoid == RelationGetRelid(rel))
continue;
/*
* If we are told not to recurse, there had better not be any
* child tables, because we can't mark the constraint on the
* parent valid unless it is valid for all child tables.
*/
if (!recurse)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("constraint must be validated on child tables too")));
/* find_all_inheritors already got lock */
childrel = table_open(childoid, NoLock);
ATExecValidateConstraint(wqueue, childrel, constrName, false,
true, lockmode);
table_close(childrel, NoLock);
}
/* Queue validation for phase 3 */
newcon = (NewConstraint *) palloc0(sizeof(NewConstraint));
newcon->name = constrName;
newcon->contype = CONSTR_CHECK;
newcon->refrelid = InvalidOid;
newcon->refindid = InvalidOid;
newcon->conid = con->oid;
val = SysCacheGetAttrNotNull(CONSTROID, tuple,
Anum_pg_constraint_conbin);
conbin = TextDatumGetCString(val);
newcon->qual = (Node *) stringToNode(conbin);
/* Find or create work queue entry for this table */
tab = ATGetQueueEntry(wqueue, rel);
tab->constraints = lappend(tab->constraints, newcon);
/*
* Invalidate relcache so that others see the new validated
* constraint.
*/
CacheInvalidateRelcache(rel);
}
/*
* Now update the catalog, while we have the door open.
*/
copyTuple = heap_copytuple(tuple);
copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
copy_con->convalidated = true;
CatalogTupleUpdate(conrel, &copyTuple->t_self, copyTuple);
InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0);
heap_freetuple(copyTuple);
ObjectAddressSet(address, ConstraintRelationId, con->oid);
}
else
address = InvalidObjectAddress; /* already validated */
systable_endscan(scan);
table_close(conrel, RowExclusiveLock);
return address;
}
/*
* transformColumnNameList - transform list of column names
*
* Lookup each name and return its attnum and, optionally, type OID
*
* Note: the name of this function suggests that it's general-purpose,
* but actually it's only used to look up names appearing in foreign-key
* clauses. The error messages would need work to use it in other cases,
* and perhaps the validity checks as well.
*/
static int
transformColumnNameList(Oid relId, List *colList,
int16 *attnums, Oid *atttypids)
{
ListCell *l;
int attnum;
attnum = 0;
foreach(l, colList)
{
char *attname = strVal(lfirst(l));
HeapTuple atttuple;
Form_pg_attribute attform;
atttuple = SearchSysCacheAttName(relId, attname);
if (!HeapTupleIsValid(atttuple))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_COLUMN),
errmsg("column \"%s\" referenced in foreign key constraint does not exist",
attname)));
attform = (Form_pg_attribute) GETSTRUCT(atttuple);
if (attform->attnum < 0)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("system columns cannot be used in foreign keys")));
if (attnum >= INDEX_MAX_KEYS)
ereport(ERROR,
(errcode(ERRCODE_TOO_MANY_COLUMNS),
errmsg("cannot have more than %d keys in a foreign key",
INDEX_MAX_KEYS)));
attnums[attnum] = attform->attnum;
if (atttypids != NULL)
atttypids[attnum] = attform->atttypid;
ReleaseSysCache(atttuple);
attnum++;
}
return attnum;
}
/*
* transformFkeyGetPrimaryKey -
*
* Look up the names, attnums, and types of the primary key attributes
* for the pkrel. Also return the index OID and index opclasses of the
* index supporting the primary key. Also return whether the index has
* WITHOUT OVERLAPS.
*
* All parameters except pkrel are output parameters. Also, the function
* return value is the number of attributes in the primary key.
*
* Used when the column list in the REFERENCES specification is omitted.
*/
static int
transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
List **attnamelist,
int16 *attnums, Oid *atttypids,
Oid *opclasses, bool *pk_has_without_overlaps)
{
List *indexoidlist;
ListCell *indexoidscan;
HeapTuple indexTuple = NULL;
Form_pg_index indexStruct = NULL;
Datum indclassDatum;
oidvector *indclass;
int i;
/*
* Get the list of index OIDs for the table from the relcache, and look up
* each one in the pg_index syscache until we find one marked primary key
* (hopefully there isn't more than one such). Insist it's valid, too.
*/
*indexOid = InvalidOid;
indexoidlist = RelationGetIndexList(pkrel);
foreach(indexoidscan, indexoidlist)
{
Oid indexoid = lfirst_oid(indexoidscan);
indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid));
if (!HeapTupleIsValid(indexTuple))
elog(ERROR, "cache lookup failed for index %u", indexoid);
indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
if (indexStruct->indisprimary && indexStruct->indisvalid)
{
/*
* Refuse to use a deferrable primary key. This is per SQL spec,
* and there would be a lot of interesting semantic problems if we
* tried to allow it.
*/
if (!indexStruct->indimmediate)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot use a deferrable primary key for referenced table \"%s\"",
RelationGetRelationName(pkrel))));
*indexOid = indexoid;
break;
}
ReleaseSysCache(indexTuple);
}
list_free(indexoidlist);
/*
* Check that we found it
*/
if (!OidIsValid(*indexOid))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("there is no primary key for referenced table \"%s\"",
RelationGetRelationName(pkrel))));
/* Must get indclass the hard way */
indclassDatum = SysCacheGetAttrNotNull(INDEXRELID, indexTuple,
Anum_pg_index_indclass);
indclass = (oidvector *) DatumGetPointer(indclassDatum);
/*
* Now build the list of PK attributes from the indkey definition (we
* assume a primary key cannot have expressional elements)
*/
*attnamelist = NIL;
for (i = 0; i < indexStruct->indnkeyatts; i++)
{
int pkattno = indexStruct->indkey.values[i];
attnums[i] = pkattno;
atttypids[i] = attnumTypeId(pkrel, pkattno);
opclasses[i] = indclass->values[i];
*attnamelist = lappend(*attnamelist,
makeString(pstrdup(NameStr(*attnumAttName(pkrel, pkattno)))));
}
*pk_has_without_overlaps = indexStruct->indisexclusion;
ReleaseSysCache(indexTuple);
return i;
}
/*
* transformFkeyCheckAttrs -
*
* Validate that the 'attnums' columns in the 'pkrel' relation are valid to
* reference as part of a foreign key constraint.
*
* Returns the OID of the unique index supporting the constraint and
* populates the caller-provided 'opclasses' array with the opclasses
* associated with the index columns. Also sets whether the index
* uses WITHOUT OVERLAPS.
*
* Raises an ERROR on validation failure.
*/
static Oid
transformFkeyCheckAttrs(Relation pkrel,
int numattrs, int16 *attnums,
bool with_period, Oid *opclasses,
bool *pk_has_without_overlaps)
{
Oid indexoid = InvalidOid;
bool found = false;
bool found_deferrable = false;
List *indexoidlist;
ListCell *indexoidscan;
int i,
j;
/*
* 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).
*/
for (i = 0; i < numattrs; i++)
{
for (j = i + 1; j < numattrs; j++)
{
if (attnums[i] == attnums[j])
ereport(ERROR,
(errcode(ERRCODE_INVALID_FOREIGN_KEY),
errmsg("foreign key referenced-columns list must not contain duplicates")));
}
}
/*
* 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.
*/
indexoidlist = RelationGetIndexList(pkrel);
foreach(indexoidscan, indexoidlist)
{
HeapTuple indexTuple;
Form_pg_index indexStruct;
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);
/*
* 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))
{
Datum indclassDatum;
oidvector *indclass;
/* Must get indclass the hard way */
indclassDatum = SysCacheGetAttrNotNull(INDEXRELID, indexTuple,
Anum_pg_index_indclass);
indclass = (oidvector *) DatumGetPointer(indclassDatum);
/*
* The given attnum list may match the index columns in any order.
* Check for a match, and extract the appropriate opclasses while
* we're at it.
*
* We know that attnums[] is duplicate-free per the test at the
* start of this function, and we checked above that the number of
* index columns agrees, so if we find a match for each attnums[]
* entry then we must have a one-to-one match in some order.
*/
for (i = 0; i < numattrs; i++)
{
found = false;
for (j = 0; j < numattrs; j++)
{
if (attnums[i] == indexStruct->indkey.values[j])
{
opclasses[i] = indclass->values[j];
found = true;
break;
}
}
if (!found)
break;
}
/* The last attribute in the index must be the PERIOD FK part */
if (found && with_period)
{
int16 periodattnum = attnums[numattrs - 1];
found = (periodattnum == indexStruct->indkey.values[numattrs - 1]);
}
/*
* Refuse to use a deferrable unique/primary key. This is per SQL
* spec, and there would be a lot of interesting semantic problems
* if we tried to allow it.
*/
if (found && !indexStruct->indimmediate)
{
/*
* Remember that we found an otherwise matching index, so that
* we can generate a more appropriate error message.
*/
found_deferrable = true;
found = false;
}
/* We need to know whether the index has WITHOUT OVERLAPS */
if (found)
*pk_has_without_overlaps = indexStruct->indisexclusion;
}
ReleaseSysCache(indexTuple);
if (found)
break;
}
if (!found)
{
if (found_deferrable)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot use a deferrable unique constraint for referenced table \"%s\"",
RelationGetRelationName(pkrel))));
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_FOREIGN_KEY),
errmsg("there is no unique constraint matching given keys for referenced table \"%s\"",
RelationGetRelationName(pkrel))));
}
list_free(indexoidlist);
return indexoid;
}
/*
* 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;
}
/*
* Permissions checks on the referenced table for ADD FOREIGN KEY
*
* Note: we have already checked that the user owns the referencing table,
* else we'd have failed much earlier; no additional checks are needed for it.
*/
static void
checkFkeyPermissions(Relation rel, int16 *attnums, int natts)
{
Oid roleid = GetUserId();
AclResult aclresult;
int i;
/* 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));
}
}
/*
* 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;
ereport(DEBUG1,
(errmsg_internal("validating foreign key constraint \"%s\"", conname)));
/*
* 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 */
/*
* 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;
/*
* 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.
*/
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))
{
LOCAL_FCINFO(fcinfo, 0);
TriggerData trigdata = {0};
CHECK_FOR_INTERRUPTS();
/*
* Make a call to the trigger function
*
* No parameters are passed, but we do set a context
*/
MemSet(fcinfo, 0, SizeForFunctionCallInfo(0));
/*
* We assume RI_FKey_check_ins won't look at flinfo...
*/
trigdata.type = T_TriggerData;
trigdata.tg_event = TRIGGER_EVENT_INSERT | TRIGGER_EVENT_ROW;
trigdata.tg_relation = rel;
trigdata.tg_trigtuple = ExecFetchSlotHeapTuple(slot, false, NULL);
trigdata.tg_trigslot = slot;
trigdata.tg_trigger = &trig;
fcinfo->context = (Node *) &trigdata;
RI_FKey_check_ins(fcinfo);
MemoryContextReset(perTupCxt);
}
MemoryContextSwitchTo(oldcxt);
MemoryContextDelete(perTupCxt);
table_endscan(scan);
UnregisterSnapshot(snapshot);
ExecDropSingleTupleTableSlot(slot);
}
/*
* 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;
/*
* 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.
*/
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)
{
fk_trigger->funcname = SystemFuncName("RI_FKey_check_ins");
fk_trigger->events = TRIGGER_TYPE_INSERT;
}
else
{
fk_trigger->funcname = SystemFuncName("RI_FKey_check_upd");
fk_trigger->events = TRIGGER_TYPE_UPDATE;
}
fk_trigger->args = NIL;
fk_trigger->row = true;
fk_trigger->timing = TRIGGER_TYPE_AFTER;
fk_trigger->columns = NIL;
fk_trigger->whenClause = NULL;
fk_trigger->transitionRels = NIL;
fk_trigger->deferrable = fkconstraint->deferrable;
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
constraintOid, indexOid, InvalidOid,
parentTrigOid, NULL, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
return trigAddress.objectId;
}
/*
* createForeignKeyActionTriggers
* Create the referenced-side "action" triggers that implement a foreign
* key.
*
* Returns the OIDs of the so created triggers in *deleteTrigOid and
* *updateTrigOid.
*/
static void
createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
Oid constraintOid, Oid indexOid,
Oid parentDelTrigger, Oid parentUpdTrigger,
Oid *deleteTrigOid, Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
ObjectAddress trigAddress;
/*
* Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON
* DELETE action on the referenced table.
*/
fk_trigger = makeNode(CreateTrigStmt);
fk_trigger->replace = false;
fk_trigger->isconstraint = true;
fk_trigger->trigname = "RI_ConstraintTrigger_a";
fk_trigger->relation = NULL;
fk_trigger->args = NIL;
fk_trigger->row = true;
fk_trigger->timing = TRIGGER_TYPE_AFTER;
fk_trigger->events = TRIGGER_TYPE_DELETE;
fk_trigger->columns = NIL;
fk_trigger->whenClause = NULL;
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
switch (fkconstraint->fk_del_action)
{
case FKCONSTR_ACTION_NOACTION:
fk_trigger->deferrable = fkconstraint->deferrable;
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_del");
break;
case FKCONSTR_ACTION_RESTRICT:
fk_trigger->deferrable = false;
fk_trigger->initdeferred = false;
fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_del");
break;
case FKCONSTR_ACTION_CASCADE:
fk_trigger->deferrable = false;
fk_trigger->initdeferred = false;
fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_del");
break;
case FKCONSTR_ACTION_SETNULL:
fk_trigger->deferrable = false;
fk_trigger->initdeferred = false;
fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_del");
break;
case FKCONSTR_ACTION_SETDEFAULT:
fk_trigger->deferrable = false;
fk_trigger->initdeferred = false;
fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_del");
break;
default:
elog(ERROR, "unrecognized FK action type: %d",
(int) fkconstraint->fk_del_action);
break;
}
trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
RelationGetRelid(rel),
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,
RelationGetRelid(rel),
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)))
{
List *readyRels = NIL;
dropconstraint_internal(rel, tuple, behavior, recurse, false,
missing_ok, &readyRels, 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, List **readyRels,
LOCKMODE lockmode)
{
Relation conrel;
Form_pg_constraint con;
ObjectAddress conobj;
List *children;
bool is_no_inherit_constraint = false;
char *constrName;
List *unconstrained_cols = NIL;
char *colname = NULL;
bool dropping_pk = false;
if (list_member_oid(*readyRels, RelationGetRelid(rel)))
return InvalidObjectAddress;
*readyRels = lappend_oid(*readyRels, RelationGetRelid(rel));
/* 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_FOREIGN_TABLE);
conrel = table_open(ConstraintRelationId, RowExclusiveLock);
con = (Form_pg_constraint) GETSTRUCT(constraintTup);
constrName = NameStr(con->conname);
/*
* If we're asked to drop a constraint which is both defined locally and
* inherited, we can simply mark it as no longer having a local
* definition, and no further changes are required.
*
* XXX We do this for not-null constraints only, not CHECK, because the
* latter have historically not behaved this way and it might be confusing
* to change the behavior now.
*/
if (con->contype == CONSTRAINT_NOTNULL &&
con->conislocal && con->coninhcount > 0)
{
HeapTuple copytup;
copytup = heap_copytuple(constraintTup);
con = (Form_pg_constraint) GETSTRUCT(copytup);
con->conislocal = false;
CatalogTupleUpdate(conrel, &copytup->t_self, copytup);
ObjectAddressSet(conobj, ConstraintRelationId, con->oid);
CommandCounterIncrement();
table_close(conrel, RowExclusiveLock);
return conobj;
}
/* 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))));
/*
* See if we have a not-null constraint or a PRIMARY KEY. If so, we have
* more checks and actions below, so obtain the list of columns that are
* constrained by the constraint being dropped.
*/
if (con->contype == CONSTRAINT_NOTNULL)
{
AttrNumber colnum;
colnum = extractNotNullColumn(constraintTup);
unconstrained_cols = list_make1_int(colnum);
colname = NameStr(TupleDescAttr(RelationGetDescr(rel),
colnum - 1)->attname);
}
else if (con->contype == CONSTRAINT_PRIMARY)
{
Datum adatum;
ArrayType *arr;
int numkeys;
bool isNull;
int16 *attnums;
dropping_pk = true;
adatum = heap_getattr(constraintTup, Anum_pg_constraint_conkey,
RelationGetDescr(conrel), &isNull);
if (isNull)
elog(ERROR, "null conkey for constraint %u", con->oid);
arr = DatumGetArrayTypeP(adatum); /* ensure not toasted */
numkeys = ARR_DIMS(arr)[0];
if (ARR_NDIM(arr) != 1 ||
numkeys < 0 ||
ARR_HASNULL(arr) ||
ARR_ELEMTYPE(arr) != INT2OID)
elog(ERROR, "conkey is not a 1-D smallint array");
attnums = (int16 *) ARR_DATA_PTR(arr);
for (int i = 0; i < numkeys; i++)
unconstrained_cols = lappend_int(unconstrained_cols, attnums[i]);
}
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);
CheckTableNotInUse(frel, "ALTER TABLE");
table_close(frel, NoLock);
}
/*
* Perform the actual constraint deletion
*/
ObjectAddressSet(conobj, ConstraintRelationId, con->oid);
performDeletion(&conobj, behavior, 0);
/*
* If this was a NOT NULL or the primary key, verify that we still have
* constraints to support GENERATED AS IDENTITY or the replica identity.
*/
if (unconstrained_cols != NIL)
{
Relation attrel;
Bitmapset *pkcols;
Bitmapset *ircols;
/* Make implicit attnotnull changes visible */
CommandCounterIncrement();
attrel = table_open(AttributeRelationId, RowExclusiveLock);
/*
* We want to test columns for their presence in the primary key, but
* only if we're not dropping it.
*/
pkcols = dropping_pk ? NULL :
RelationGetIndexAttrBitmap(rel,
INDEX_ATTR_BITMAP_PRIMARY_KEY);
ircols = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_IDENTITY_KEY);
foreach_int(attnum, unconstrained_cols)
{
HeapTuple atttup;
HeapTuple contup;
Form_pg_attribute attForm;
char attidentity;
/*
* Obtain pg_attribute tuple and verify conditions on it.
*/
atttup = SearchSysCacheAttNum(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);
attidentity = attForm->attidentity;
ReleaseSysCache(atttup);
/*
* Since the above deletion has been made visible, we can now
* search for any remaining constraints on this column (or these
* columns, in the case we're dropping a multicol primary key.)
* Then, verify whether any further NOT NULL or primary key
* exists: if none and this is a generated identity column or the
* replica identity, abort the whole thing.
*/
contup = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
if (contup ||
bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber,
pkcols))
continue;
/*
* It's not valid to drop the not-null constraint for a GENERATED
* AS IDENTITY column.
*/
if (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)));
/*
* It's not valid to drop the not-null constraint for a column in
* the replica identity index, either. (FULL is not affected.)
*/
if (bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, ircols))
ereport(ERROR,
errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("column \"%s\" is in index used as replica identity",
get_attname(RelationGetRelid(rel), attnum, false)));
}
table_close(attrel, RowExclusiveLock);
}
/*
* 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;
/*
* For a partitioned table, if partitions exist and we are told not to
* recurse, it's a user error. It doesn't make sense to have a constraint
* be defined only on the parent, especially if it's a partitioned table.
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
children != NIL && !recurse)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("cannot remove constraint from only the partitioned table when partitions exist"),
errhint("Do not specify the ONLY keyword.")));
foreach_oid(childrelid, children)
{
Relation childrel;
HeapTuple tuple;
Form_pg_constraint childcon;
if (list_member_oid(*readyRels, childrelid))
continue; /* child already processed */
/* find_inheritance_children already got lock */
childrel = table_open(childrelid, NoLock);
CheckTableNotInUse(childrel, "ALTER TABLE");
/*
* 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, readyRels,
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);
}
/*
* In addition, when dropping a primary key from a legacy-inheritance
* parent table, we must recurse to children to mark the corresponding NOT
* NULL constraint as no longer inherited, or drop it if this its last
* reference.
*/
if (con->contype == CONSTRAINT_PRIMARY &&
rel->rd_rel->relkind == RELKIND_RELATION &&
rel->rd_rel->relhassubclass)
{
List *colnames = NIL;
List *pkready = NIL;
/*
* Because primary keys are always marked as NO INHERIT, we don't have
* a list of children yet, so obtain one now.
*/
children = find_inheritance_children(RelationGetRelid(rel), lockmode);
/*
* Find out the list of column names to process. Fortunately, we
* already have the list of column numbers.
*/
foreach_int(attnum, unconstrained_cols)
{
colnames = lappend(colnames, get_attname(RelationGetRelid(rel),
attnum, false));
}
foreach_oid(childrelid, children)
{
Relation childrel;
if (list_member_oid(pkready, childrelid))
continue; /* child already processed */
/* find_inheritance_children already got lock */
childrel = table_open(childrelid, NoLock);
CheckTableNotInUse(childrel, "ALTER TABLE");
foreach_ptr(char, colName, colnames)
{
HeapTuple contup;
contup = findNotNullConstraint(childrelid, colName);
if (contup == NULL)
elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\", relation \"%s\"",
colName, RelationGetRelationName(childrel));
dropconstraint_internal(childrel, contup,
DROP_RESTRICT, true, true,
false, &pkready,
lockmode);
pkready = NIL;
}
table_close(childrel, NoLock);
pkready = lappend_oid(pkready, childrelid);
}
}
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;
if (rel->rd_rel->reloftype && !recursing)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot alter column type of typed table")));
/* 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))));
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)));
/*
* 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)));
/* 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))));
/* Look up the target type */
typenameTypeIdAndMod(NULL, 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(NULL, def, targettype);
/* make sure datatype is legal for a column */
CheckAttributeType(colName, targettype, targetcollid,
list_make1_oid(rel->rd_rel->reltype),
0);
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)),
/* 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))));
}
/* Fix collations after all else */
assign_expr_collations(pstate, transform);
/* 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 = (NewColumnValue *) palloc0(sizeof(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))
{
/*
* For relations 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);
CheckTableNotInUse(childrel, "ALTER TABLE");
/*
* 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);
StoreAttrDefault(rel, attnum, defaultexpr, true, false);
}
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;
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;
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 FOREIGN KEY constraint, we might not yet have exclusive
* lock on the table the constraint is attached to, and we need to get
* that before reparsing/dropping.
*
* 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;
char contype;
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;
contype = con->contype;
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 an FK constraint that references the table we're
* modifying, we might not yet have any lock on the FK's table, so get
* one now. We'll need AccessExclusiveLock for the DROP CONSTRAINT
* step, so there's no value in asking for anything weaker.
*/
if (relid != tab->relid && contype == CONSTRAINT_FOREIGN)
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);
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);
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 */
RebuildConstraintComment(tab,
AT_PASS_OLD_CONSTR,
oldId,
rel,
NIL,
con->conname);
}
else if (cmd->subtype == AT_SetAttNotNull)
{
/*
* We see this subtype when a primary key is created
* internally, for example when it is replaced with a new
* constraint (say because one of the columns changes
* type); in this case we need to reinstate attnotnull,
* because it was removed because of the drop of the old
* PK. Schedule this subcommand to an upcoming AT pass.
*/
cmd->subtype = AT_SetAttNotNull;
tab->subcmds[AT_PASS_OLD_COL_ATTRS] =
lappend(tab->subcmds[AT_PASS_OLD_COL_ATTRS], cmd);
}
else
elog(ERROR, "unexpected statement subtype: %d",
(int) cmd->subtype);
}
}
else if (IsA(stm, AlterDomainStmt))
{
AlterDomainStmt *stmt = (AlterDomainStmt *) stm;
if (stmt->subtype == 'C') /* ADD CONSTRAINT */
{
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 (PointerIsValid(DatumGetPointer(datum)))
repl_val[Anum_pg_attribute_attfdwoptions - 1] = datum;
else
repl_null[Anum_pg_attribute_attfdwoptions - 1] = true;
repl_repl[Anum_pg_attribute_attfdwoptions - 1] = true;
/* Everything looks good - update the tuple */
newtuple = heap_modify_tuple(tuple, RelationGetDescr(attrel),
repl_val, repl_null, repl_repl);
CatalogTupleUpdate(attrel, &newtuple->t_self, newtuple);
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel),
atttableform->attnum);
ObjectAddressSubSet(address, RelationRelationId,
RelationGetRelid(rel), attnum);
ReleaseSysCache(tuple);
table_close(attrel, RowExclusiveLock);
heap_freetuple(newtuple);
return address;
}
/*
* ALTER TABLE OWNER
*
* recursing is true if we are recursing from a table to its indexes,
* sequences, or toast table. We don't allow the ownership of those things to
* be changed separately from the parent table. Also, we can skip permission
* checks (this is necessary not just an optimization, else we'd fail to
* handle toast tables properly).
*
* recursing is also true if ALTER TYPE OWNER is calling us to fix up a
* free-standing composite type.
*/
void
ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lockmode)
{
Relation target_rel;
Relation class_rel;
HeapTuple tuple;
Form_pg_class tuple_class;
/*
* Get exclusive lock till end of transaction on the target table. Use
* relation_open so that we can work on indexes and sequences.
*/
target_rel = relation_open(relationOid, lockmode);
/* Get its pg_class tuple, too */
class_rel = table_open(RelationRelationId, RowExclusiveLock);
tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relationOid));
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for relation %u", relationOid);
tuple_class = (Form_pg_class) GETSTRUCT(tuple);
/* Can we change the ownership of this tuple? */
switch (tuple_class->relkind)
{
case RELKIND_RELATION:
case RELKIND_VIEW:
case RELKIND_MATVIEW:
case RELKIND_FOREIGN_TABLE:
case RELKIND_PARTITIONED_TABLE:
/* ok to change owner */
break;
case RELKIND_INDEX:
if (!recursing)
{
/*
* Because ALTER INDEX OWNER used to be allowed, and in fact
* is generated by old versions of pg_dump, we give a warning
* and do nothing rather than erroring out. Also, to avoid
* unnecessary chatter while restoring those old dumps, say
* nothing at all if the command would be a no-op anyway.
*/
if (tuple_class->relowner != newOwnerId)
ereport(WARNING,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot change owner of index \"%s\"",
NameStr(tuple_class->relname)),
errhint("Change the ownership of the index's table instead.")));
/* quick hack to exit via the no-op path */
newOwnerId = tuple_class->relowner;
}
break;
case RELKIND_PARTITIONED_INDEX:
if (recursing)
break;
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot change owner of index \"%s\"",
NameStr(tuple_class->relname)),
errhint("Change the ownership of the index's table instead.")));
break;
case RELKIND_SEQUENCE:
if (!recursing &&
tuple_class->relowner != newOwnerId)
{
/* if it's an owned sequence, disallow changing it by itself */
Oid tableId;
int32 colId;
if (sequenceIsOwned(relationOid, DEPENDENCY_AUTO, &tableId, &colId) ||
sequenceIsOwned(relationOid, DEPENDENCY_INTERNAL, &tableId, &colId))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot change owner of sequence \"%s\"",
NameStr(tuple_class->relname)),
errdetail("Sequence \"%s\" is linked to table \"%s\".",
NameStr(tuple_class->relname),
get_rel_name(tableId))));
}
break;
case RELKIND_COMPOSITE_TYPE:
if (recursing)
break;
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is a composite type",
NameStr(tuple_class->relname)),
/* translator: %s is an SQL ALTER command */
errhint("Use %s instead.",
"ALTER TYPE")));
break;
case RELKIND_TOASTVALUE:
if (recursing)
break;
/* 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())
{
Oid namespaceOid = tuple_class->relnamespace;
AclResult aclresult;
/* Otherwise, must be owner of the existing object */
if (!object_ownercheck(RelationRelationId, relationOid, GetUserId()))
aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relationOid)),
RelationGetRelationName(target_rel));
/* Must be able to become new owner */
check_can_set_role(GetUserId(), newOwnerId);
/* New owner must have CREATE privilege on namespace */
aclresult = object_aclcheck(NamespaceRelationId, namespaceOid, newOwnerId,
ACL_CREATE);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult, OBJECT_SCHEMA,
get_namespace_name(namespaceOid));
}
}
memset(repl_null, false, sizeof(repl_null));
memset(repl_repl, false, sizeof(repl_repl));
repl_repl[Anum_pg_class_relowner - 1] = true;
repl_val[Anum_pg_class_relowner - 1] = ObjectIdGetDatum(newOwnerId);
/*
* Determine the modified ACL for the new owner. This is only
* necessary when the ACL is non-null.
*/
aclDatum = SysCacheGetAttr(RELOID, tuple,
Anum_pg_class_relacl,
&isNull);
if (!isNull)
{
newAcl = aclnewowner(DatumGetAclP(aclDatum),
tuple_class->relowner, newOwnerId);
repl_repl[Anum_pg_class_relacl - 1] = true;
repl_val[Anum_pg_class_relacl - 1] = PointerGetDatum(newAcl);
}
newtuple = heap_modify_tuple(tuple, RelationGetDescr(class_rel), repl_val, repl_null, repl_repl);
CatalogTupleUpdate(class_rel, &newtuple->t_self, newtuple);
heap_freetuple(newtuple);
/*
* We must similarly update any per-column ACLs to reflect the new
* owner; for neatness reasons that's split out as a subroutine.
*/
change_owner_fix_column_acls(relationOid,
tuple_class->relowner,
newOwnerId);
/*
* Update owner dependency reference, if any. A composite type has
* none, because it's tracked for the pg_type entry instead of here;
* indexes and TOAST tables don't have their own entries either.
*/
if (tuple_class->relkind != RELKIND_COMPOSITE_TYPE &&
tuple_class->relkind != RELKIND_INDEX &&
tuple_class->relkind != RELKIND_PARTITIONED_INDEX &&
tuple_class->relkind != RELKIND_TOASTVALUE)
changeDependencyOnOwner(RelationRelationId, relationOid,
newOwnerId);
/*
* Also change the ownership of the table's row type, if it has one
*/
if (OidIsValid(tuple_class->reltype))
AlterTypeOwnerInternal(tuple_class->reltype, newOwnerId);
/*
* If we are operating on a table or materialized view, also change
* the ownership of any indexes and sequences that belong to the
* relation, as well as its toast table (if it has one).
*/
if (tuple_class->relkind == RELKIND_RELATION ||
tuple_class->relkind == RELKIND_PARTITIONED_TABLE ||
tuple_class->relkind == RELKIND_MATVIEW ||
tuple_class->relkind == RELKIND_TOASTVALUE)
{
List *index_oid_list;
ListCell *i;
/* Find all the indexes belonging to this relation */
index_oid_list = RelationGetIndexList(target_rel);
/* For each index, recursively change its ownership */
foreach(i, index_oid_list)
ATExecChangeOwner(lfirst_oid(i), newOwnerId, true, lockmode);
list_free(index_oid_list);
}
/* If it has a toast table, recurse to change its ownership */
if (tuple_class->reltoastrelid != InvalidOid)
ATExecChangeOwner(tuple_class->reltoastrelid, newOwnerId,
true, lockmode);
/* If it has dependent sequences, recurse to change them too */
change_owner_recurse_to_sequences(relationOid, newOwnerId, lockmode);
}
InvokeObjectPostAlterHook(RelationRelationId, relationOid, 0);
ReleaseSysCache(tuple);
table_close(class_rel, RowExclusiveLock);
relation_close(target_rel, NoLock);
}
/*
* change_owner_fix_column_acls
*
* Helper function for ATExecChangeOwner. Scan the columns of the table
* and fix any non-null column ACLs to reflect the new owner.
*/
static void
change_owner_fix_column_acls(Oid relationOid, Oid oldOwnerId, Oid newOwnerId)
{
Relation attRelation;
SysScanDesc scan;
ScanKeyData key[1];
HeapTuple attributeTuple;
attRelation = table_open(AttributeRelationId, RowExclusiveLock);
ScanKeyInit(&key[0],
Anum_pg_attribute_attrelid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(relationOid));
scan = systable_beginscan(attRelation, AttributeRelidNumIndexId,
true, NULL, 1, key);
while (HeapTupleIsValid(attributeTuple = systable_getnext(scan)))
{
Form_pg_attribute att = (Form_pg_attribute) GETSTRUCT(attributeTuple);
Datum repl_val[Natts_pg_attribute];
bool repl_null[Natts_pg_attribute];
bool repl_repl[Natts_pg_attribute];
Acl *newAcl;
Datum aclDatum;
bool isNull;
HeapTuple newtuple;
/* Ignore dropped columns */
if (att->attisdropped)
continue;
aclDatum = heap_getattr(attributeTuple,
Anum_pg_attribute_attacl,
RelationGetDescr(attRelation),
&isNull);
/* Null ACLs do not require changes */
if (isNull)
continue;
memset(repl_null, false, sizeof(repl_null));
memset(repl_repl, false, sizeof(repl_repl));
newAcl = aclnewowner(DatumGetAclP(aclDatum),
oldOwnerId, newOwnerId);
repl_repl[Anum_pg_attribute_attacl - 1] = true;
repl_val[Anum_pg_attribute_attacl - 1] = PointerGetDatum(newAcl);
newtuple = heap_modify_tuple(attributeTuple,
RelationGetDescr(attRelation),
repl_val, repl_null, repl_repl);
CatalogTupleUpdate(attRelation, &newtuple->t_self, newtuple);
heap_freetuple(newtuple);
}
systable_endscan(scan);
table_close(attRelation, RowExclusiveLock);
}
/*
* change_owner_recurse_to_sequences
*
* Helper function for ATExecChangeOwner. Examines pg_depend searching
* for sequences that are dependent on serial columns, and changes their
* ownership.
*/
static void
change_owner_recurse_to_sequences(Oid relationOid, Oid newOwnerId, LOCKMODE lockmode)
{
Relation depRel;
SysScanDesc scan;
ScanKeyData key[2];
HeapTuple tup;
/*
* SERIAL sequences are those having an auto dependency on one of the
* table's columns (we don't care *which* column, exactly).
*/
depRel = table_open(DependRelationId, AccessShareLock);
ScanKeyInit(&key[0],
Anum_pg_depend_refclassid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(RelationRelationId));
ScanKeyInit(&key[1],
Anum_pg_depend_refobjid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(relationOid));
/* we leave refobjsubid unspecified */
scan = systable_beginscan(depRel, DependReferenceIndexId, true,
NULL, 2, key);
while (HeapTupleIsValid(tup = systable_getnext(scan)))
{
Form_pg_depend depForm = (Form_pg_depend) GETSTRUCT(tup);
Relation seqRel;
/* skip dependencies other than auto dependencies on columns */
if (depForm->refobjsubid == 0 ||
depForm->classid != RelationRelationId ||
depForm->objsubid != 0 ||
!(depForm->deptype == DEPENDENCY_AUTO || depForm->deptype == DEPENDENCY_INTERNAL))
continue;
/* Use relation_open just in case it's an index */
seqRel = relation_open(depForm->objid, lockmode);
/* skip non-sequence relations */
if (RelationGetForm(seqRel)->relkind != RELKIND_SEQUENCE)
{
/* No need to keep the lock */
relation_close(seqRel, lockmode);
continue;
}
/* We don't need to close the sequence while we alter it. */
ATExecChangeOwner(depForm->objid, newOwnerId, true, lockmode);
/* Now we can close it. Keep the lock till end of transaction. */
relation_close(seqRel, NoLock);
}
systable_endscan(scan);
relation_close(depRel, AccessShareLock);
}
/*
* ALTER TABLE CLUSTER ON
*
* The only thing we have to do is to change the indisclustered bits.
*
* Return the address of the new clustering index.
*/
static ObjectAddress
ATExecClusterOn(Relation rel, const char *indexName, LOCKMODE lockmode)
{
Oid indexOid;
ObjectAddress address;
indexOid = get_relname_relid(indexName, rel->rd_rel->relnamespace);
if (!OidIsValid(indexOid))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("index \"%s\" for table \"%s\" does not exist",
indexName, RelationGetRelationName(rel))));
/* Check index is valid to cluster on */
check_index_is_clusterable(rel, indexOid, lockmode);
/* And do the work */
mark_index_clustered(rel, indexOid, false);
ObjectAddressSet(address,
RelationRelationId, indexOid);
return address;
}
/*
* ALTER TABLE SET WITHOUT CLUSTER
*
* We have to find any indexes on the table that have indisclustered bit
* set and turn it off.
*/
static void
ATExecDropCluster(Relation rel, LOCKMODE lockmode)
{
mark_index_clustered(rel, InvalidOid, false);
}
/*
* Preparation phase for SET ACCESS METHOD
*
* Check that the access method exists and determine whether a change is
* actually needed.
*/
static void
ATPrepSetAccessMethod(AlteredTableInfo *tab, Relation rel, const char *amname)
{
Oid amoid;
/*
* Look up the access method name and check that it differs from the
* table's current AM. If DEFAULT was specified for a partitioned table
* (amname is NULL), set it to InvalidOid to reset the catalogued AM.
*/
if (amname != NULL)
amoid = get_table_am_oid(amname, false);
else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
amoid = InvalidOid;
else
amoid = get_table_am_oid(default_table_access_method, false);
/* if it's a match, phase 3 doesn't need to do anything */
if (rel->rd_rel->relam == amoid)
return;
/* Save info for Phase 3 to do the real work */
tab->rewrite |= AT_REWRITE_ACCESS_METHOD;
tab->newAccessMethod = amoid;
tab->chgAccessMethod = true;
}
/*
* Special handling of ALTER TABLE SET ACCESS METHOD for relations with no
* storage that have an interest in preserving AM.
*
* Since these have no storage, setting the access method is a catalog only
* operation.
*/
static void
ATExecSetAccessMethodNoStorage(Relation rel, Oid newAccessMethodId)
{
Relation pg_class;
Oid oldAccessMethodId;
HeapTuple tuple;
Form_pg_class rd_rel;
Oid reloid = RelationGetRelid(rel);
/*
* Shouldn't be called on relations having storage; these are processed in
* phase 3.
*/
Assert(!RELKIND_HAS_STORAGE(rel->rd_rel->relkind));
/* Get a modifiable copy of the relation's pg_class row. */
pg_class = table_open(RelationRelationId, RowExclusiveLock);
tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for relation %u", reloid);
rd_rel = (Form_pg_class) GETSTRUCT(tuple);
/* Update the pg_class row. */
oldAccessMethodId = rd_rel->relam;
rd_rel->relam = newAccessMethodId;
/* Leave if no update required */
if (rd_rel->relam == oldAccessMethodId)
{
heap_freetuple(tuple);
table_close(pg_class, RowExclusiveLock);
return;
}
CatalogTupleUpdate(pg_class, &tuple->t_self, tuple);
/*
* Update the dependency on the new access method. No dependency is added
* if the new access method is InvalidOid (default case). Be very careful
* that this has to compare the previous value stored in pg_class with the
* new one.
*/
if (!OidIsValid(oldAccessMethodId) && OidIsValid(rd_rel->relam))
{
ObjectAddress relobj,
referenced;
/*
* New access method is defined and there was no dependency
* previously, so record a new one.
*/
ObjectAddressSet(relobj, RelationRelationId, reloid);
ObjectAddressSet(referenced, AccessMethodRelationId, rd_rel->relam);
recordDependencyOn(&relobj, &referenced, DEPENDENCY_NORMAL);
}
else if (OidIsValid(oldAccessMethodId) &&
!OidIsValid(rd_rel->relam))
{
/*
* There was an access method defined, and no new one, so just remove
* the existing dependency.
*/
deleteDependencyRecordsForClass(RelationRelationId, reloid,
AccessMethodRelationId,
DEPENDENCY_NORMAL);
}
else
{
Assert(OidIsValid(oldAccessMethodId) &&
OidIsValid(rd_rel->relam));
/* Both are valid, so update the dependency */
changeDependencyFor(RelationRelationId, reloid,
AccessMethodRelationId,
oldAccessMethodId, rd_rel->relam);
}
/* make the relam and dependency changes visible */
CommandCounterIncrement();
InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0);
heap_freetuple(tuple);
table_close(pg_class, RowExclusiveLock);
}
/*
* ALTER TABLE SET TABLESPACE
*/
static void
ATPrepSetTableSpace(AlteredTableInfo *tab, Relation rel, const char *tablespacename, LOCKMODE lockmode)
{
Oid tablespaceId;
/* Check that the tablespace exists */
tablespaceId = get_tablespace_oid(tablespacename, false);
/* Check permissions except when moving to database's default */
if (OidIsValid(tablespaceId) && tablespaceId != MyDatabaseTableSpace)
{
AclResult aclresult;
aclresult = object_aclcheck(TableSpaceRelationId, tablespaceId, GetUserId(), ACL_CREATE);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult, OBJECT_TABLESPACE, tablespacename);
}
/* Save info for Phase 3 to do the real work */
if (OidIsValid(tab->newTableSpace))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("cannot have multiple SET TABLESPACE subcommands")));
tab->newTableSpace = tablespaceId;
}
/*
* Set, reset, or replace reloptions.
*/
static void
ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
LOCKMODE lockmode)
{
Oid relid;
Relation pgclass;
HeapTuple tuple;
HeapTuple newtuple;
Datum datum;
bool isnull;
Datum newOptions;
Datum repl_val[Natts_pg_class];
bool repl_null[Natts_pg_class];
bool repl_repl[Natts_pg_class];
static char *validnsps[] = HEAP_RELOPT_NAMESPACES;
if (defList == NIL && operation != AT_ReplaceRelOptions)
return; /* nothing to do */
pgclass = table_open(RelationRelationId, RowExclusiveLock);
/* Fetch heap tuple */
relid = RelationGetRelid(rel);
tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for relation %u", relid);
if (operation == AT_ReplaceRelOptions)
{
/*
* If we're supposed to replace the reloptions list, we just pretend
* there were none before.
*/
datum = (Datum) 0;
isnull = true;
}
else
{
/* Get the old reloptions */
datum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions,
&isnull);
}
/* Generate new proposed reloptions (text array) */
newOptions = transformRelOptions(isnull ? (Datum) 0 : datum,
defList, NULL, validnsps, false,
operation == AT_ResetRelOptions);
/* Validate */
switch (rel->rd_rel->relkind)
{
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
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;
default:
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot set options for relation \"%s\"",
RelationGetRelationName(rel)),
errdetail_relkind_not_supported(rel->rd_rel->relkind)));
break;
}
/* Special-case validation of view options */
if (rel->rd_rel->relkind == RELKIND_VIEW)
{
Query *view_query = get_view_query(rel);
List *view_options = untransformRelOptions(newOptions);
ListCell *cell;
bool check_option = false;
foreach(cell, view_options)
{
DefElem *defel = (DefElem *) lfirst(cell);
if (strcmp(defel->defname, "check_option") == 0)
check_option = true;
}
/*
* If the check option is specified, look to see if the view is
* actually auto-updatable or not.
*/
if (check_option)
{
const char *view_updatable_error =
view_query_is_auto_updatable(view_query, true);
if (view_updatable_error)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("WITH CHECK OPTION is supported only on automatically updatable views"),
errhint("%s", _(view_updatable_error))));
}
}
/*
* All we need do here is update the pg_class row; the new options will be
* propagated into relcaches during post-commit cache inval.
*/
memset(repl_val, 0, sizeof(repl_val));
memset(repl_null, false, sizeof(repl_null));
memset(repl_repl, false, sizeof(repl_repl));
if (newOptions != (Datum) 0)
repl_val[Anum_pg_class_reloptions - 1] = newOptions;
else
repl_null[Anum_pg_class_reloptions - 1] = true;
repl_repl[Anum_pg_class_reloptions - 1] = true;
newtuple = heap_modify_tuple(tuple, RelationGetDescr(pgclass),
repl_val, repl_null, repl_repl);
CatalogTupleUpdate(pgclass, &newtuple->t_self, newtuple);
InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0);
heap_freetuple(newtuple);
ReleaseSysCache(tuple);
/* repeat the whole exercise for the toast table, if there's one */
if (OidIsValid(rel->rd_rel->reltoastrelid))
{
Relation toastrel;
Oid toastid = rel->rd_rel->reltoastrelid;
toastrel = table_open(toastid, lockmode);
/* Fetch heap tuple */
tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(toastid));
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for relation %u", toastid);
if (operation == AT_ReplaceRelOptions)
{
/*
* If we're supposed to replace the reloptions list, we just
* pretend there were none before.
*/
datum = (Datum) 0;
isnull = true;
}
else
{
/* Get the old reloptions */
datum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions,
&isnull);
}
newOptions = transformRelOptions(isnull ? (Datum) 0 : datum,
defList, "toast", validnsps, false,
operation == AT_ResetRelOptions);
(void) heap_reloptions(RELKIND_TOASTVALUE, newOptions, true);
memset(repl_val, 0, sizeof(repl_val));
memset(repl_null, false, sizeof(repl_null));
memset(repl_repl, false, sizeof(repl_repl));
if (newOptions != (Datum) 0)
repl_val[Anum_pg_class_reloptions - 1] = newOptions;
else
repl_null[Anum_pg_class_reloptions - 1] = true;
repl_repl[Anum_pg_class_reloptions - 1] = true;
newtuple = heap_modify_tuple(tuple, RelationGetDescr(pgclass),
repl_val, repl_null, repl_repl);
CatalogTupleUpdate(pgclass, &newtuple->t_self, newtuple);
InvokeObjectPostAlterHookArg(RelationRelationId,
RelationGetRelid(toastrel), 0,
InvalidOid, true);
heap_freetuple(newtuple);
ReleaseSysCache(tuple);
table_close(toastrel, NoLock);
}
table_close(pgclass, RowExclusiveLock);
}
/*
* Execute ALTER TABLE SET TABLESPACE for cases where there is no tuple
* rewriting to be done, so we just want to copy the data as fast as possible.
*/
static void
ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode)
{
Relation rel;
Oid reltoastrelid;
RelFileNumber newrelfilenumber;
RelFileLocator newrlocator;
List *reltoastidxids = NIL;
ListCell *lc;
/*
* Need lock here in case we are recursing to toast table or index
*/
rel = relation_open(tableOid, lockmode);
/* Check first if relation can be moved to new tablespace */
if (!CheckRelationTableSpaceMove(rel, newTableSpace))
{
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel), 0);
relation_close(rel, NoLock);
return;
}
reltoastrelid = rel->rd_rel->reltoastrelid;
/* Fetch the list of indexes on toast relation if necessary */
if (OidIsValid(reltoastrelid))
{
Relation toastRel = relation_open(reltoastrelid, lockmode);
reltoastidxids = RelationGetIndexList(toastRel);
relation_close(toastRel, lockmode);
}
/*
* Relfilenumbers are not unique in databases across tablespaces, so we
* need to allocate a new one in the new tablespace.
*/
newrelfilenumber = GetNewRelFileNumber(newTableSpace, NULL,
rel->rd_rel->relpersistence);
/* Open old and new relation */
newrlocator = rel->rd_locator;
newrlocator.relNumber = newrelfilenumber;
newrlocator.spcOid = newTableSpace;
/* hand off to AM to actually create new rel storage and copy the data */
if (rel->rd_rel->relkind == RELKIND_INDEX)
{
index_copy_data(rel, newrlocator);
}
else
{
Assert(RELKIND_HAS_TABLE_AM(rel->rd_rel->relkind));
table_relation_copy_data(rel, &newrlocator);
}
/*
* Update the pg_class row.
*
* NB: This wouldn't work if ATExecSetTableSpace() were allowed to be
* executed on pg_class or its indexes (the above copy wouldn't contain
* the updated pg_class entry), but that's forbidden with
* CheckRelationTableSpaceMove().
*/
SetRelationTableSpace(rel, newTableSpace, newrelfilenumber);
InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0);
RelationAssumeNewRelfilelocator(rel);
relation_close(rel, NoLock);
/* Make sure the reltablespace change is visible */
CommandCounterIncrement();
/* Move associated toast relation and/or indexes, too */
if (OidIsValid(reltoastrelid))
ATExecSetTableSpace(reltoastrelid, newTableSpace, lockmode);
foreach(lc, reltoastidxids)
ATExecSetTableSpace(lfirst_oid(lc), newTableSpace, lockmode);
/* Clean up */
list_free(reltoastidxids);
}
/*
* Special handling of ALTER TABLE SET TABLESPACE for relations with no
* storage that have an interest in preserving tablespace.
*
* Since these have no storage the tablespace can be updated with a simple
* metadata only operation to update the tablespace.
*/
static void
ATExecSetTableSpaceNoStorage(Relation rel, Oid newTableSpace)
{
/*
* Shouldn't be called on relations having storage; these are processed in
* phase 3.
*/
Assert(!RELKIND_HAS_STORAGE(rel->rd_rel->relkind));
/* check if relation can be moved to its new tablespace */
if (!CheckRelationTableSpaceMove(rel, newTableSpace))
{
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel),
0);
return;
}
/* Update can be done, so change reltablespace */
SetRelationTableSpace(rel, newTableSpace, InvalidOid);
InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0);
/* Make sure the reltablespace change is visible */
CommandCounterIncrement();
}
/*
* Alter Table ALL ... SET TABLESPACE
*
* Allows a user to move all objects of some type in a given tablespace in the
* current database to another tablespace. Objects can be chosen based on the
* owner of the object also, to allow users to move only their objects.
* The user must have CREATE rights on the new tablespace, as usual. The main
* permissions handling is done by the lower-level table move function.
*
* All to-be-moved objects are locked first. If NOWAIT is specified and the
* lock can't be acquired then we ereport(ERROR).
*/
Oid
AlterTableMoveAll(AlterTableMoveAllStmt *stmt)
{
List *relations = NIL;
ListCell *l;
ScanKeyData key[1];
Relation rel;
TableScanDesc scan;
HeapTuple tuple;
Oid orig_tablespaceoid;
Oid new_tablespaceoid;
List *role_oids = roleSpecsToIds(stmt->roles);
/* Ensure we were not asked to move something we can't */
if (stmt->objtype != OBJECT_TABLE && stmt->objtype != OBJECT_INDEX &&
stmt->objtype != OBJECT_MATVIEW)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("only tables, indexes, and materialized views exist in tablespaces")));
/* Get the orig and new tablespace OIDs */
orig_tablespaceoid = get_tablespace_oid(stmt->orig_tablespacename, false);
new_tablespaceoid = get_tablespace_oid(stmt->new_tablespacename, false);
/* Can't move shared relations in to or out of pg_global */
/* This is also checked by ATExecSetTableSpace, but nice to stop earlier */
if (orig_tablespaceoid == GLOBALTABLESPACE_OID ||
new_tablespaceoid == GLOBALTABLESPACE_OID)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("cannot move relations in to or out of pg_global tablespace")));
/*
* Must have CREATE rights on the new tablespace, unless it is the
* database default tablespace (which all users implicitly have CREATE
* rights on).
*/
if (OidIsValid(new_tablespaceoid) && new_tablespaceoid != MyDatabaseTableSpace)
{
AclResult aclresult;
aclresult = object_aclcheck(TableSpaceRelationId, new_tablespaceoid, GetUserId(),
ACL_CREATE);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult, OBJECT_TABLESPACE,
get_tablespace_name(new_tablespaceoid));
}
/*
* Now that the checks are done, check if we should set either to
* InvalidOid because it is our database's default tablespace.
*/
if (orig_tablespaceoid == MyDatabaseTableSpace)
orig_tablespaceoid = InvalidOid;
if (new_tablespaceoid == MyDatabaseTableSpace)
new_tablespaceoid = InvalidOid;
/* no-op */
if (orig_tablespaceoid == new_tablespaceoid)
return new_tablespaceoid;
/*
* Walk the list of objects in the tablespace and move them. This will
* only find objects in our database, of course.
*/
ScanKeyInit(&key[0],
Anum_pg_class_reltablespace,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(orig_tablespaceoid));
rel = table_open(RelationRelationId, AccessShareLock);
scan = table_beginscan_catalog(rel, 1, key);
while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relOid = relForm->oid;
/*
* Do not move objects in pg_catalog as part of this, if an admin
* really wishes to do so, they can issue the individual ALTER
* commands directly.
*
* Also, explicitly avoid any shared tables, temp tables, or TOAST
* (TOAST will be moved with the main table).
*/
if (IsCatalogNamespace(relForm->relnamespace) ||
relForm->relisshared ||
isAnyTempNamespace(relForm->relnamespace) ||
IsToastNamespace(relForm->relnamespace))
continue;
/* Only move the object type requested */
if ((stmt->objtype == OBJECT_TABLE &&
relForm->relkind != RELKIND_RELATION &&
relForm->relkind != RELKIND_PARTITIONED_TABLE) ||
(stmt->objtype == OBJECT_INDEX &&
relForm->relkind != RELKIND_INDEX &&
relForm->relkind != RELKIND_PARTITIONED_INDEX) ||
(stmt->objtype == OBJECT_MATVIEW &&
relForm->relkind != RELKIND_MATVIEW))
continue;
/* Check if we are only moving objects owned by certain roles */
if (role_oids != NIL && !list_member_oid(role_oids, relForm->relowner))
continue;
/*
* Handle permissions-checking here since we are locking the tables
* and also to avoid doing a bunch of work only to fail part-way. Note
* that permissions will also be checked by AlterTableInternal().
*
* Caller must be considered an owner on the table to move it.
*/
if (!object_ownercheck(RelationRelationId, relOid, GetUserId()))
aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relOid)),
NameStr(relForm->relname));
if (stmt->nowait &&
!ConditionalLockRelationOid(relOid, AccessExclusiveLock))
ereport(ERROR,
(errcode(ERRCODE_OBJECT_IN_USE),
errmsg("aborting because lock on relation \"%s.%s\" is not available",
get_namespace_name(relForm->relnamespace),
NameStr(relForm->relname))));
else
LockRelationOid(relOid, AccessExclusiveLock);
/* Add to our list of objects to move */
relations = lappend_oid(relations, relOid);
}
table_endscan(scan);
table_close(rel, AccessShareLock);
if (relations == NIL)
ereport(NOTICE,
(errcode(ERRCODE_NO_DATA_FOUND),
errmsg("no matching relations in tablespace \"%s\" found",
orig_tablespaceoid == InvalidOid ? "(database default)" :
get_tablespace_name(orig_tablespaceoid))));
/* Everything is locked, loop through and move all of the relations. */
foreach(l, relations)
{
List *cmds = NIL;
AlterTableCmd *cmd = makeNode(AlterTableCmd);
cmd->subtype = AT_SetTableSpace;
cmd->name = stmt->new_tablespacename;
cmds = lappend(cmds, cmd);
EventTriggerAlterTableStart((Node *) stmt);
/* OID is set by AlterTableInternal */
AlterTableInternal(lfirst_oid(l), cmds, false);
EventTriggerAlterTableEnd();
}
return new_tablespaceoid;
}
static void
index_copy_data(Relation rel, RelFileLocator newrlocator)
{
SMgrRelation dstrel;
/*
* Since we copy the file directly without looking at the shared buffers,
* we'd better first flush out any pages of the source relation that are
* in shared buffers. We assume no new changes will be made while we are
* holding exclusive lock on the rel.
*/
FlushRelationBuffers(rel);
/*
* Create and copy all forks of the relation, and schedule unlinking of
* old physical files.
*
* NOTE: any conflict in relfilenumber value will be caught in
* RelationCreateStorage().
*/
dstrel = RelationCreateStorage(newrlocator, rel->rd_rel->relpersistence, true);
/* copy main fork */
RelationCopyStorage(RelationGetSmgr(rel), dstrel, MAIN_FORKNUM,
rel->rd_rel->relpersistence);
/* copy those extra forks that exist */
for (ForkNumber forkNum = MAIN_FORKNUM + 1;
forkNum <= MAX_FORKNUM; forkNum++)
{
if (smgrexists(RelationGetSmgr(rel), forkNum))
{
smgrcreate(dstrel, forkNum, false);
/*
* WAL log creation if the relation is persistent, or this is the
* init fork of an unlogged relation.
*/
if (RelationIsPermanent(rel) ||
(rel->rd_rel->relpersistence == RELPERSISTENCE_UNLOGGED &&
forkNum == INIT_FORKNUM))
log_smgrcreate(&newrlocator, forkNum);
RelationCopyStorage(RelationGetSmgr(rel), dstrel, forkNum,
rel->rd_rel->relpersistence);
}
}
/* drop old relation, and close new one */
RelationDropStorage(rel);
smgrclose(dstrel);
}
/*
* ALTER TABLE ENABLE/DISABLE TRIGGER
*
* We just pass this off to trigger.c.
*/
static void
ATExecEnableDisableTrigger(Relation rel, const char *trigname,
char fires_when, bool skip_system, bool recurse,
LOCKMODE lockmode)
{
EnableDisableTrigger(rel, trigname, InvalidOid,
fires_when, skip_system, recurse,
lockmode);
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel), 0);
}
/*
* ALTER TABLE ENABLE/DISABLE RULE
*
* We just pass this off to rewriteDefine.c.
*/
static void
ATExecEnableDisableRule(Relation rel, const char *rulename,
char fires_when, LOCKMODE lockmode)
{
EnableDisableRule(rel, rulename, fires_when);
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel), 0);
}
/*
* 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 (child_rel->rd_rel->relispartition)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot change inheritance of a partition")));
if (child_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot change inheritance of partitioned table")));
}
/*
* Return the address of the new parent relation.
*/
static ObjectAddress
ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode)
{
Relation parent_rel;
List *children;
ObjectAddress address;
const char *trigger_name;
/*
* A self-exclusive lock is needed here. See the similar case in
* MergeAttributes() for a full explanation.
*/
parent_rel = table_openrv(parent, ShareUpdateExclusiveLock);
/*
* Must be owner of both parent and child -- child was checked by
* ATSimplePermissions call in ATPrepCmd
*/
ATSimplePermissions(AT_AddInherit, parent_rel, ATT_TABLE | ATT_FOREIGN_TABLE);
/* Permanent rels cannot inherit from temporary ones */
if (parent_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
child_rel->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot inherit from temporary relation \"%s\"",
RelationGetRelationName(parent_rel))));
/* If parent rel is temp, it must belong to this session */
if (parent_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
!parent_rel->rd_islocaltemp)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot inherit from temporary relation of another session")));
/* Ditto for the child */
if (child_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
!child_rel->rd_islocaltemp)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot inherit to temporary relation of another session")));
/* Prevent partitioned tables from becoming inheritance parents */
if (parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot inherit from partitioned table \"%s\"",
parent->relname)));
/* Likewise for partitions */
if (parent_rel->rd_rel->relispartition)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot inherit from a partition")));
/*
* Prevent circularity by seeing if proposed parent inherits from child.
* (In particular, this disallows making a rel inherit from itself.)
*
* This is not completely bulletproof because of race conditions: in
* multi-level inheritance trees, someone else could concurrently be
* making another inheritance link that closes the loop but does not join
* either of the rels we have locked. Preventing that seems to require
* exclusive locks on the entire inheritance tree, which is a cure worse
* than the disease. find_all_inheritors() will cope with circularity
* anyway, so don't sweat it too much.
*
* We use weakest lock we can on child's children, namely AccessShareLock.
*/
children = find_all_inheritors(RelationGetRelid(child_rel),
AccessShareLock, NULL);
if (list_member_oid(children, RelationGetRelid(parent_rel)))
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_TABLE),
errmsg("circular inheritance not allowed"),
errdetail("\"%s\" is already a child of \"%s\".",
parent->relname,
RelationGetRelationName(child_rel))));
/*
* If child_rel has row-level triggers with transition tables, we
* currently don't allow it to become an inheritance child. See also
* prohibitions in ATExecAttachPartition() and CreateTrigger().
*/
trigger_name = FindTriggerIncompatibleWithInheritance(child_rel->trigdesc);
if (trigger_name != NULL)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("trigger \"%s\" prevents table \"%s\" from becoming an inheritance child",
trigger_name, RelationGetRelationName(child_rel)),
errdetail("ROW triggers with transition tables are not supported in inheritance hierarchies.")));
/* OK to create inheritance */
CreateInheritance(child_rel, parent_rel, false);
/*
* If parent_rel has a primary key, then child_rel has not-null
* constraints that make these columns as non nullable. Make those
* constraints as inherited.
*/
ATInheritAdjustNotNulls(parent_rel, child_rel, 1);
ObjectAddressSet(address, RelationRelationId,
RelationGetRelid(parent_rel));
/* keep our lock on the parent relation until commit */
table_close(parent_rel, NoLock);
return address;
}
/*
* CreateInheritance
* Catalog manipulation portion of creating inheritance between a child
* table and a parent table.
*
* Common to ATExecAddInherit() and ATExecAttachPartition().
*/
static void
CreateInheritance(Relation child_rel, Relation parent_rel, bool ispartition)
{
Relation catalogRelation;
SysScanDesc scan;
ScanKeyData key;
HeapTuple inheritsTuple;
int32 inhseqno;
/* Note: get RowExclusiveLock because we will write pg_inherits below. */
catalogRelation = table_open(InheritsRelationId, RowExclusiveLock);
/*
* Check for duplicates in the list of parents, and determine the highest
* inhseqno already present; we'll use the next one for the new parent.
* Also, if proposed child is a partition, it cannot already be
* inheriting.
*
* Note: we do not reject the case where the child already inherits from
* the parent indirectly; CREATE TABLE doesn't reject comparable cases.
*/
ScanKeyInit(&key,
Anum_pg_inherits_inhrelid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(RelationGetRelid(child_rel)));
scan = systable_beginscan(catalogRelation, InheritsRelidSeqnoIndexId,
true, NULL, 1, &key);
/* inhseqno sequences start at 1 */
inhseqno = 0;
while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan)))
{
Form_pg_inherits inh = (Form_pg_inherits) GETSTRUCT(inheritsTuple);
if (inh->inhparent == RelationGetRelid(parent_rel))
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_TABLE),
errmsg("relation \"%s\" would be inherited from more than once",
RelationGetRelationName(parent_rel))));
if (inh->inhseqno > inhseqno)
inhseqno = inh->inhseqno;
}
systable_endscan(scan);
/* Match up the columns and bump attinhcount as needed */
MergeAttributesIntoExisting(child_rel, parent_rel, ispartition);
/* Match up the constraints and bump coninhcount as needed */
MergeConstraintsIntoExisting(child_rel, parent_rel);
/*
* OK, it looks valid. Make the catalog entries that show inheritance.
*/
StoreCatalogInheritance1(RelationGetRelid(child_rel),
RelationGetRelid(parent_rel),
inhseqno + 1,
catalogRelation,
parent_rel->rd_rel->relkind ==
RELKIND_PARTITIONED_TABLE);
/* Now we're done with pg_inherits */
table_close(catalogRelation, RowExclusiveLock);
}
/*
* Obtain the source-text form of the constraint expression for a check
* constraint, given its pg_constraint tuple
*/
static char *
decompile_conbin(HeapTuple contup, TupleDesc tupdesc)
{
Form_pg_constraint con;
bool isnull;
Datum attr;
Datum expr;
con = (Form_pg_constraint) GETSTRUCT(contup);
attr = heap_getattr(contup, Anum_pg_constraint_conbin, tupdesc, &isnull);
if (isnull)
elog(ERROR, "null conbin for constraint %u", con->oid);
expr = DirectFunctionCall2(pg_get_expr, attr,
ObjectIdGetDatum(con->conrelid));
return TextDatumGetCString(expr);
}
/*
* Determine whether two check constraints are functionally equivalent
*
* The test we apply is to see whether they reverse-compile to the same
* source string. This insulates us from issues like whether attributes
* have the same physical column numbers in parent and child relations.
*/
static bool
constraints_equivalent(HeapTuple a, HeapTuple b, TupleDesc tupleDesc)
{
Form_pg_constraint acon = (Form_pg_constraint) GETSTRUCT(a);
Form_pg_constraint bcon = (Form_pg_constraint) GETSTRUCT(b);
if (acon->condeferrable != bcon->condeferrable ||
acon->condeferred != bcon->condeferred ||
strcmp(decompile_conbin(a, tupleDesc),
decompile_conbin(b, tupleDesc)) != 0)
return false;
else
return true;
}
/*
* Check columns in child table match up with columns in parent, and increment
* their attinhcount.
*
* Called by CreateInheritance
*
* Currently all parent columns must be found in child. Missing columns are an
* error. One day we might consider creating new columns like CREATE TABLE
* does. However, that is widely unpopular --- in the common use case of
* partitioned tables it's a foot-gun.
*
* The data type must match exactly. If the parent column is NOT NULL then
* the child must be as well. Defaults are not compared, however.
*/
static void
MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispartition)
{
Relation attrrel;
TupleDesc parent_desc;
attrrel = table_open(AttributeRelationId, RowExclusiveLock);
parent_desc = RelationGetDescr(parent_rel);
for (AttrNumber parent_attno = 1; parent_attno <= parent_desc->natts; parent_attno++)
{
Form_pg_attribute parent_att = TupleDescAttr(parent_desc, parent_attno - 1);
char *parent_attname = NameStr(parent_att->attname);
HeapTuple tuple;
/* Ignore dropped columns in the parent. */
if (parent_att->attisdropped)
continue;
/* Find same column in child (matching on column name). */
tuple = SearchSysCacheCopyAttName(RelationGetRelid(child_rel), parent_attname);
if (HeapTupleIsValid(tuple))
{
Form_pg_attribute child_att = (Form_pg_attribute) GETSTRUCT(tuple);
if (parent_att->atttypid != child_att->atttypid ||
parent_att->atttypmod != child_att->atttypmod)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("child table \"%s\" has different type for column \"%s\"",
RelationGetRelationName(child_rel), parent_attname)));
if (parent_att->attcollation != child_att->attcollation)
ereport(ERROR,
(errcode(ERRCODE_COLLATION_MISMATCH),
errmsg("child table \"%s\" has different collation for column \"%s\"",
RelationGetRelationName(child_rel), parent_attname)));
/*
* If the parent has a not-null constraint that's not NO INHERIT,
* make sure the child has one too.
*
* Other constraints are checked elsewhere.
*/
if (parent_att->attnotnull && !child_att->attnotnull)
{
HeapTuple contup;
contup = findNotNullConstraintAttnum(RelationGetRelid(parent_rel),
parent_att->attnum);
if (HeapTupleIsValid(contup) &&
!((Form_pg_constraint) GETSTRUCT(contup))->connoinherit)
ereport(ERROR,
errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("column \"%s\" in child table must be marked NOT NULL",
parent_attname));
}
/*
* 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)));
/*
* Regular inheritance children are independent enough not to
* inherit identity columns. But partitions are integral part of
* a partitioned table and inherit identity column.
*/
if (ispartition)
child_att->attidentity = parent_att->attidentity;
/*
* OK, bump the child column's inheritance count. (If we fail
* later on, this change will just roll back.)
*/
child_att->attinhcount++;
if (child_att->attinhcount < 0)
ereport(ERROR,
errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("too many inheritance parents"));
/*
* In case of partitions, we must enforce that value of attislocal
* is same in all partitions. (Note: there are only inherited
* attributes in partitions)
*/
if (parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
Assert(child_att->attinhcount == 1);
child_att->attislocal = false;
}
CatalogTupleUpdate(attrrel, &tuple->t_self, tuple);
heap_freetuple(tuple);
}
else
{
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("child table is missing column \"%s\"", parent_attname)));
}
}
table_close(attrrel, RowExclusiveLock);
}
/*
* Check constraints in child table match up with constraints in parent,
* and increment their coninhcount.
*
* Constraints that are marked ONLY in the parent are ignored.
*
* Called by CreateInheritance
*
* Currently all constraints in parent must be present in the child. One day we
* may consider adding new constraints like CREATE TABLE does.
*
* XXX This is O(N^2) which may be an issue with tables with hundreds of
* constraints. As long as tables have more like 10 constraints it shouldn't be
* a problem though. Even 100 constraints ought not be the end of the world.
*
* XXX See MergeWithExistingConstraint too if you change this code.
*/
static void
MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
{
Relation constraintrel;
SysScanDesc parent_scan;
ScanKeyData parent_key;
HeapTuple parent_tuple;
Oid parent_relid = RelationGetRelid(parent_rel);
constraintrel = table_open(ConstraintRelationId, RowExclusiveLock);
/* Outer loop scans through the parent's constraint definitions */
ScanKeyInit(&parent_key,
Anum_pg_constraint_conrelid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(parent_relid));
parent_scan = systable_beginscan(constraintrel, ConstraintRelidTypidNameIndexId,
true, NULL, 1, &parent_key);
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;
bool found = false;
if (parent_con->contype != CONSTRAINT_CHECK &&
parent_con->contype != CONSTRAINT_NOTNULL)
continue;
/* if the parent's constraint is marked NO INHERIT, it's not inherited */
if (parent_con->connoinherit)
continue;
/* 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 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)
{
AttrNumber parent_attno = extractNotNullColumn(parent_tuple);
AttrNumber child_attno = extractNotNullColumn(child_tuple);
if (strcmp(get_attname(parent_relid, parent_attno, false),
get_attname(RelationGetRelid(child_rel), child_attno,
false)) != 0)
continue;
}
if (child_con->contype == CONSTRAINT_CHECK &&
!constraints_equivalent(parent_tuple, child_tuple, RelationGetDescr(constraintrel)))
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("child table \"%s\" has different definition for check constraint \"%s\"",
RelationGetRelationName(child_rel), NameStr(parent_con->conname))));
/*
* If the CHECK child constraint is "no inherit" then cannot
* merge.
*
* This is not desirable for not-null constraints, mostly because
* it breaks our pg_upgrade strategy, but it also makes sense on
* its own: if a child has its own not-null constraint and then
* acquires a parent with the same constraint, then we start to
* enforce that constraint for all the descendants of that child
* too, if any.
*/
if (child_con->contype == CONSTRAINT_CHECK &&
child_con->connoinherit)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with non-inherited constraint on child table \"%s\"",
NameStr(child_con->conname), RelationGetRelationName(child_rel))));
/*
* If the child constraint is "not valid" then cannot merge with a
* valid parent constraint
*/
if (parent_con->convalidated && !child_con->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))));
/*
* 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);
child_con->coninhcount++;
if (child_con->coninhcount < 0)
ereport(ERROR,
errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("too many inheritance parents"));
if (child_con->contype == CONSTRAINT_NOTNULL &&
child_con->connoinherit)
{
/*
* If the child has children, it's not possible to turn a NO
* INHERIT constraint into an inheritable one: we would need
* to recurse to create constraints in those children, but
* this is not a good place to do that.
*/
if (child_rel->rd_rel->relhassubclass)
ereport(ERROR,
errmsg("cannot add NOT NULL constraint to column \"%s\" of relation \"%s\" with inheritance children",
get_attname(RelationGetRelid(child_rel),
extractNotNullColumn(child_tuple),
false),
RelationGetRelationName(child_rel)),
errdetail("Existing constraint \"%s\" is marked NO INHERIT.",
NameStr(child_con->conname)));
child_con->connoinherit = false;
}
/*
* In case of partitions, an inherited constraint must be
* inherited only once since it cannot have multiple parents and
* it is never considered local.
*/
if (parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
Assert(child_con->coninhcount == 1);
child_con->conislocal = false;
}
CatalogTupleUpdate(constraintrel, &child_copy->t_self, child_copy);
heap_freetuple(child_copy);
found = true;
break;
}
systable_endscan(child_scan);
if (!found)
{
if (parent_con->contype == CONSTRAINT_NOTNULL)
ereport(ERROR,
errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("column \"%s\" in child table must be marked NOT NULL",
get_attname(parent_relid,
extractNotNullColumn(parent_tuple),
false)));
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("child table is missing constraint \"%s\"",
NameStr(parent_con->conname))));
}
}
systable_endscan(parent_scan);
table_close(constraintrel, RowExclusiveLock);
}
/*
* ALTER TABLE NO INHERIT
*
* Return value is the address of the relation that is no longer parent.
*/
static ObjectAddress
ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode)
{
ObjectAddress address;
Relation parent_rel;
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);
/*
* We don't bother to check ownership of the parent table --- ownership of
* the child is presumed enough rights.
*/
/* Off to RemoveInheritance() where most of the work happens */
RemoveInheritance(rel, parent_rel, false);
/*
* If parent_rel has a primary key, then child_rel has not-null
* constraints that make these columns as non nullable. Mark those
* constraints as no longer inherited by this parent.
*/
ATInheritAdjustNotNulls(parent_rel, rel, -1);
/*
* If the parent has a primary key, then we decrement counts for all NOT
* NULL constraints
*/
ObjectAddressSet(address, RelationRelationId,
RelationGetRelid(parent_rel));
/* keep our lock on the parent relation until commit */
table_close(parent_rel, NoLock);
return address;
}
/*
* MarkInheritDetached
*
* Set inhdetachpending for a partition, for ATExecDetachPartition
* in concurrent mode. While at it, verify that no other partition is
* already pending detach.
*/
static void
MarkInheritDetached(Relation child_rel, Relation parent_rel)
{
Relation catalogRelation;
SysScanDesc scan;
ScanKeyData key;
HeapTuple inheritsTuple;
bool found = false;
Assert(parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
/*
* Find pg_inherits entries by inhparent. (We need to scan them all in
* order to verify that no other partition is pending detach.)
*/
catalogRelation = table_open(InheritsRelationId, RowExclusiveLock);
ScanKeyInit(&key,
Anum_pg_inherits_inhparent,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(RelationGetRelid(parent_rel)));
scan = systable_beginscan(catalogRelation, InheritsParentIndexId,
true, NULL, 1, &key);
while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan)))
{
Form_pg_inherits inhForm;
inhForm = (Form_pg_inherits) GETSTRUCT(inheritsTuple);
if (inhForm->inhdetachpending)
ereport(ERROR,
errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("partition \"%s\" already pending detach in partitioned table \"%s.%s\"",
get_rel_name(inhForm->inhrelid),
get_namespace_name(parent_rel->rd_rel->relnamespace),
RelationGetRelationName(parent_rel)),
errhint("Use ALTER TABLE ... DETACH PARTITION ... FINALIZE to complete the pending detach operation."));
if (inhForm->inhrelid == RelationGetRelid(child_rel))
{
HeapTuple newtup;
newtup = heap_copytuple(inheritsTuple);
((Form_pg_inherits) GETSTRUCT(newtup))->inhdetachpending = true;
CatalogTupleUpdate(catalogRelation,
&inheritsTuple->t_self,
newtup);
found = true;
heap_freetuple(newtup);
/* keep looking, to ensure we catch others pending detach */
}
}
/* Done */
systable_endscan(scan);
table_close(catalogRelation, RowExclusiveLock);
if (!found)
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_TABLE),
errmsg("relation \"%s\" is not a partition of relation \"%s\"",
RelationGetRelationName(child_rel),
RelationGetRelationName(parent_rel))));
}
/*
* RemoveInheritance
*
* Drop a parent from the child's parents. This just adjusts the attinhcount
* and attislocal of the columns and removes the pg_inherit and pg_depend
* entries. expect_detached is passed down to DeleteInheritsTuple, q.v..
*
* If attinhcount goes to 0 then attislocal gets set to true. If it goes back
* up attislocal stays true, which means if a child is ever removed from a
* parent then its columns will never be automatically dropped which may
* surprise. But at least we'll never surprise by dropping columns someone
* isn't expecting to be dropped which would actually mean data loss.
*
* coninhcount and conislocal for inherited constraints are adjusted in
* exactly the same way.
*
* Common to ATExecDropInherit() and ATExecDetachPartition().
*/
static void
RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached)
{
Relation catalogRelation;
SysScanDesc scan;
ScanKeyData key[3];
HeapTuple attributeTuple,
constraintTuple;
List *connames;
List *nncolumns;
bool found;
bool is_partitioning;
is_partitioning = (parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
found = DeleteInheritsTuple(RelationGetRelid(child_rel),
RelationGetRelid(parent_rel),
expect_detached,
RelationGetRelationName(child_rel));
if (!found)
{
if (is_partitioning)
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_TABLE),
errmsg("relation \"%s\" is not a partition of relation \"%s\"",
RelationGetRelationName(child_rel),
RelationGetRelationName(parent_rel))));
else
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_TABLE),
errmsg("relation \"%s\" is not a parent of relation \"%s\"",
RelationGetRelationName(parent_rel),
RelationGetRelationName(child_rel))));
}
/*
* Search through child columns looking for ones matching parent rel
*/
catalogRelation = table_open(AttributeRelationId, RowExclusiveLock);
ScanKeyInit(&key[0],
Anum_pg_attribute_attrelid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(RelationGetRelid(child_rel)));
scan = systable_beginscan(catalogRelation, AttributeRelidNumIndexId,
true, NULL, 1, key);
while (HeapTupleIsValid(attributeTuple = systable_getnext(scan)))
{
Form_pg_attribute att = (Form_pg_attribute) GETSTRUCT(attributeTuple);
/* Ignore if dropped or not inherited */
if (att->attisdropped)
continue;
if (att->attinhcount <= 0)
continue;
if (SearchSysCacheExistsAttName(RelationGetRelid(parent_rel),
NameStr(att->attname)))
{
/* Decrement inhcount and possibly set islocal to true */
HeapTuple copyTuple = heap_copytuple(attributeTuple);
Form_pg_attribute copy_att = (Form_pg_attribute) GETSTRUCT(copyTuple);
copy_att->attinhcount--;
if (copy_att->attinhcount == 0)
copy_att->attislocal = true;
CatalogTupleUpdate(catalogRelation, &copyTuple->t_self, copyTuple);
heap_freetuple(copyTuple);
}
}
systable_endscan(scan);
table_close(catalogRelation, RowExclusiveLock);
/*
* Likewise, find inherited check 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.
*/
catalogRelation = table_open(ConstraintRelationId, RowExclusiveLock);
ScanKeyInit(&key[0],
Anum_pg_constraint_conrelid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(RelationGetRelid(parent_rel)));
scan = systable_beginscan(catalogRelation, ConstraintRelidTypidNameIndexId,
true, NULL, 1, key);
connames = NIL;
nncolumns = NIL;
while (HeapTupleIsValid(constraintTuple = systable_getnext(scan)))
{
Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(constraintTuple);
if (con->contype == CONSTRAINT_CHECK)
connames = lappend(connames, pstrdup(NameStr(con->conname)));
if (con->contype == CONSTRAINT_NOTNULL)
nncolumns = lappend_int(nncolumns, extractNotNullColumn(constraintTuple));
}
systable_endscan(scan);
/* Now scan the child's constraints */
ScanKeyInit(&key[0],
Anum_pg_constraint_conrelid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(RelationGetRelid(child_rel)));
scan = systable_beginscan(catalogRelation, ConstraintRelidTypidNameIndexId,
true, NULL, 1, key);
while (HeapTupleIsValid(constraintTuple = systable_getnext(scan)))
{
Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(constraintTuple);
bool match = false;
ListCell *lc;
/*
* Match CHECK constraints by name, not-null constraints by column
* number, and ignore all others.
*/
if (con->contype == CONSTRAINT_CHECK)
{
foreach(lc, connames)
{
if (con->contype == CONSTRAINT_CHECK &&
strcmp(NameStr(con->conname), (char *) lfirst(lc)) == 0)
{
match = true;
break;
}
}
}
else if (con->contype == CONSTRAINT_NOTNULL)
{
AttrNumber child_attno = extractNotNullColumn(constraintTuple);
foreach(lc, nncolumns)
{
if (lfirst_int(lc) == child_attno)
{
match = true;
break;
}
}
}
else
continue;
if (match)
{
/* Decrement inhcount and possibly set islocal to true */
HeapTuple copyTuple = heap_copytuple(constraintTuple);
Form_pg_constraint copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
if (copy_con->coninhcount <= 0) /* shouldn't happen */
elog(ERROR, "relation %u has non-inherited constraint \"%s\"",
RelationGetRelid(child_rel), NameStr(copy_con->conname));
copy_con->coninhcount--;
if (copy_con->coninhcount == 0)
copy_con->conislocal = true;
CatalogTupleUpdate(catalogRelation, &copyTuple->t_self, copyTuple);
heap_freetuple(copyTuple);
}
}
systable_endscan(scan);
table_close(catalogRelation, RowExclusiveLock);
drop_parent_dependency(RelationGetRelid(child_rel),
RelationRelationId,
RelationGetRelid(parent_rel),
child_dependency_type(is_partitioning));
/*
* Post alter hook of this inherits. Since object_access_hook doesn't take
* multiple object identifiers, we relay oid of parent relation using
* auxiliary_id argument.
*/
InvokeObjectPostAlterHookArg(InheritsRelationId,
RelationGetRelid(child_rel), 0,
RelationGetRelid(parent_rel), false);
}
/*
* Adjust coninhcount of not-null constraints upwards or downwards when a
* table is marked as inheriting or no longer doing so a table with a primary
* key.
*
* Note: these constraints are not dropped, even if their inhcount goes to zero
* and conislocal is false. Instead we mark the constraints as locally defined.
* This is seen as more useful behavior, with no downsides. The user can always
* drop them afterwards.
*/
static void
ATInheritAdjustNotNulls(Relation parent_rel, Relation child_rel, int inhcount)
{
Bitmapset *pkattnos;
/* Quick exit when parent has no PK */
if (!parent_rel->rd_rel->relhasindex)
return;
pkattnos = RelationGetIndexAttrBitmap(parent_rel,
INDEX_ATTR_BITMAP_PRIMARY_KEY);
if (pkattnos != NULL)
{
Bitmapset *childattnums = NULL;
AttrMap *attmap;
int i;
attmap = build_attrmap_by_name(RelationGetDescr(parent_rel),
RelationGetDescr(child_rel), true);
i = -1;
while ((i = bms_next_member(pkattnos, i)) >= 0)
{
childattnums = bms_add_member(childattnums,
attmap->attnums[i + FirstLowInvalidHeapAttributeNumber - 1]);
}
/*
* CCI is needed in case there's a NOT NULL PRIMARY KEY column in the
* parent: the relevant not-null constraint in the child already had
* its inhcount modified earlier.
*/
CommandCounterIncrement();
AdjustNotNullInheritance(RelationGetRelid(child_rel), childattnums,
inhcount);
}
}
/*
* Drop the dependency created by StoreCatalogInheritance1 (CREATE TABLE
* INHERITS/ALTER TABLE INHERIT -- refclassid will be RelationRelationId) or
* heap_create_with_catalog (CREATE TABLE OF/ALTER TABLE OF -- refclassid will
* be TypeRelationId). There's no convenient way to do this, so go trawling
* through pg_depend.
*/
static void
drop_parent_dependency(Oid relid, Oid refclassid, Oid refobjid,
DependencyType deptype)
{
Relation catalogRelation;
SysScanDesc scan;
ScanKeyData key[3];
HeapTuple depTuple;
catalogRelation = table_open(DependRelationId, RowExclusiveLock);
ScanKeyInit(&key[0],
Anum_pg_depend_classid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(RelationRelationId));
ScanKeyInit(&key[1],
Anum_pg_depend_objid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(relid));
ScanKeyInit(&key[2],
Anum_pg_depend_objsubid,
BTEqualStrategyNumber, F_INT4EQ,
Int32GetDatum(0));
scan = systable_beginscan(catalogRelation, DependDependerIndexId, true,
NULL, 3, key);
while (HeapTupleIsValid(depTuple = systable_getnext(scan)))
{
Form_pg_depend dep = (Form_pg_depend) GETSTRUCT(depTuple);
if (dep->refclassid == refclassid &&
dep->refobjid == refobjid &&
dep->refobjsubid == 0 &&
dep->deptype == deptype)
CatalogTupleDelete(catalogRelation, &depTuple->t_self);
}
systable_endscan(scan);
table_close(catalogRelation, RowExclusiveLock);
}
/*
* ALTER TABLE OF
*
* Attach a table to a composite type, as though it had been created with CREATE
* TABLE OF. All attname, atttypid, atttypmod and attcollation must match. The
* subject table must not have inheritance parents. These restrictions ensure
* that you cannot create a configuration impossible with CREATE TABLE OF alone.
*
* The address of the type is returned.
*/
static ObjectAddress
ATExecAddOf(Relation rel, const TypeName *ofTypename, LOCKMODE lockmode)
{
Oid relid = RelationGetRelid(rel);
Type typetuple;
Form_pg_type typeform;
Oid typeid;
Relation inheritsRelation,
relationRelation;
SysScanDesc scan;
ScanKeyData key;
AttrNumber table_attno,
type_attno;
TupleDesc typeTupleDesc,
tableTupleDesc;
ObjectAddress tableobj,
typeobj;
HeapTuple classtuple;
/* Validate the type. */
typetuple = typenameType(NULL, ofTypename, NULL);
check_of_type(typetuple);
typeform = (Form_pg_type) GETSTRUCT(typetuple);
typeid = typeform->oid;
/* Fail if the table has any inheritance parents. */
inheritsRelation = table_open(InheritsRelationId, AccessShareLock);
ScanKeyInit(&key,
Anum_pg_inherits_inhrelid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(relid));
scan = systable_beginscan(inheritsRelation, InheritsRelidSeqnoIndexId,
true, NULL, 1, &key);
if (HeapTupleIsValid(systable_getnext(scan)))
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("typed tables cannot inherit")));
systable_endscan(scan);
table_close(inheritsRelation, AccessShareLock);
/*
* Check the tuple descriptors for compatibility. Unlike inheritance, we
* require that the order also match. However, attnotnull need not match.
*/
typeTupleDesc = lookup_rowtype_tupdesc(typeid, -1);
tableTupleDesc = RelationGetDescr(rel);
table_attno = 1;
for (type_attno = 1; type_attno <= typeTupleDesc->natts; type_attno++)
{
Form_pg_attribute type_attr,
table_attr;
const char *type_attname,
*table_attname;
/* Get the next non-dropped type attribute. */
type_attr = TupleDescAttr(typeTupleDesc, type_attno - 1);
if (type_attr->attisdropped)
continue;
type_attname = NameStr(type_attr->attname);
/* Get the next non-dropped table attribute. */
do
{
if (table_attno > tableTupleDesc->natts)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("table is missing column \"%s\"",
type_attname)));
table_attr = TupleDescAttr(tableTupleDesc, table_attno - 1);
table_attno++;
} while (table_attr->attisdropped);
table_attname = NameStr(table_attr->attname);
/* Compare name. */
if (strncmp(table_attname, type_attname, NAMEDATALEN) != 0)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("table has column \"%s\" where type requires \"%s\"",
table_attname, type_attname)));
/* Compare type. */
if (table_attr->atttypid != type_attr->atttypid ||
table_attr->atttypmod != type_attr->atttypmod ||
table_attr->attcollation != type_attr->attcollation)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("table \"%s\" has different type for column \"%s\"",
RelationGetRelationName(rel), type_attname)));
}
ReleaseTupleDesc(typeTupleDesc);
/* Any remaining columns at the end of the table had better be dropped. */
for (; table_attno <= tableTupleDesc->natts; table_attno++)
{
Form_pg_attribute table_attr = TupleDescAttr(tableTupleDesc,
table_attno - 1);
if (!table_attr->attisdropped)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("table has extra column \"%s\"",
NameStr(table_attr->attname))));
}
/* If the table was already typed, drop the existing dependency. */
if (rel->rd_rel->reloftype)
drop_parent_dependency(relid, TypeRelationId, rel->rd_rel->reloftype,
DEPENDENCY_NORMAL);
/* Record a dependency on the new type. */
tableobj.classId = RelationRelationId;
tableobj.objectId = relid;
tableobj.objectSubId = 0;
typeobj.classId = TypeRelationId;
typeobj.objectId = typeid;
typeobj.objectSubId = 0;
recordDependencyOn(&tableobj, &typeobj, DEPENDENCY_NORMAL);
/* Update pg_class.reloftype */
relationRelation = table_open(RelationRelationId, RowExclusiveLock);
classtuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
if (!HeapTupleIsValid(classtuple))
elog(ERROR, "cache lookup failed for relation %u", relid);
((Form_pg_class) GETSTRUCT(classtuple))->reloftype = typeid;
CatalogTupleUpdate(relationRelation, &classtuple->t_self, classtuple);
InvokeObjectPostAlterHook(RelationRelationId, relid, 0);
heap_freetuple(classtuple);
table_close(relationRelation, RowExclusiveLock);
ReleaseSysCache(typetuple);
return typeobj;
}
/*
* ALTER TABLE NOT OF
*
* Detach a typed table from its originating type. Just clear reloftype and
* remove the dependency.
*/
static void
ATExecDropOf(Relation rel, LOCKMODE lockmode)
{
Oid relid = RelationGetRelid(rel);
Relation relationRelation;
HeapTuple tuple;
if (!OidIsValid(rel->rd_rel->reloftype))
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not a typed table",
RelationGetRelationName(rel))));
/*
* We don't bother to check ownership of the type --- ownership of the
* table is presumed enough rights. No lock required on the type, either.
*/
drop_parent_dependency(relid, TypeRelationId, rel->rd_rel->reloftype,
DEPENDENCY_NORMAL);
/* Clear pg_class.reloftype */
relationRelation = table_open(RelationRelationId, RowExclusiveLock);
tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for relation %u", relid);
((Form_pg_class) GETSTRUCT(tuple))->reloftype = InvalidOid;
CatalogTupleUpdate(relationRelation, &tuple->t_self, tuple);
InvokeObjectPostAlterHook(RelationRelationId, relid, 0);
heap_freetuple(tuple);
table_close(relationRelation, RowExclusiveLock);
}
/*
* relation_mark_replica_identity: Update a table's replica identity
*
* Iff ri_type = REPLICA_IDENTITY_INDEX, indexOid must be the Oid of a suitable
* index. Otherwise, it must be InvalidOid.
*
* Caller had better hold an exclusive lock on the relation, as the results
* of running two of these concurrently wouldn't be pretty.
*/
static void
relation_mark_replica_identity(Relation rel, char ri_type, Oid indexOid,
bool is_internal)
{
Relation pg_index;
Relation pg_class;
HeapTuple pg_class_tuple;
HeapTuple pg_index_tuple;
Form_pg_class pg_class_form;
Form_pg_index pg_index_form;
ListCell *index;
/*
* Check whether relreplident has changed, and update it if so.
*/
pg_class = table_open(RelationRelationId, RowExclusiveLock);
pg_class_tuple = SearchSysCacheCopy1(RELOID,
ObjectIdGetDatum(RelationGetRelid(rel)));
if (!HeapTupleIsValid(pg_class_tuple))
elog(ERROR, "cache lookup failed for relation \"%s\"",
RelationGetRelationName(rel));
pg_class_form = (Form_pg_class) GETSTRUCT(pg_class_tuple);
if (pg_class_form->relreplident != ri_type)
{
pg_class_form->relreplident = ri_type;
CatalogTupleUpdate(pg_class, &pg_class_tuple->t_self, pg_class_tuple);
}
table_close(pg_class, RowExclusiveLock);
heap_freetuple(pg_class_tuple);
/*
* Update the per-index indisreplident flags correctly.
*/
pg_index = table_open(IndexRelationId, RowExclusiveLock);
foreach(index, RelationGetIndexList(rel))
{
Oid thisIndexOid = lfirst_oid(index);
bool dirty = false;
pg_index_tuple = SearchSysCacheCopy1(INDEXRELID,
ObjectIdGetDatum(thisIndexOid));
if (!HeapTupleIsValid(pg_index_tuple))
elog(ERROR, "cache lookup failed for index %u", thisIndexOid);
pg_index_form = (Form_pg_index) GETSTRUCT(pg_index_tuple);
if (thisIndexOid == indexOid)
{
/* Set the bit if not already set. */
if (!pg_index_form->indisreplident)
{
dirty = true;
pg_index_form->indisreplident = true;
}
}
else
{
/* Unset the bit if set. */
if (pg_index_form->indisreplident)
{
dirty = true;
pg_index_form->indisreplident = false;
}
}
if (dirty)
{
CatalogTupleUpdate(pg_index, &pg_index_tuple->t_self, pg_index_tuple);
InvokeObjectPostAlterHookArg(IndexRelationId, thisIndexOid, 0,
InvalidOid, is_internal);
/*
* Invalidate the relcache for the table, so that after we commit
* all sessions will refresh the table's replica identity index
* before attempting any UPDATE or DELETE on the table. (If we
* changed the table's pg_class row above, then a relcache inval
* is already queued due to that; but we might not have.)
*/
CacheInvalidateRelcache(rel);
}
heap_freetuple(pg_index_tuple);
}
table_close(pg_index, RowExclusiveLock);
}
/*
* ALTER TABLE <name> REPLICA IDENTITY ...
*/
static void
ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode)
{
Oid indexOid;
Relation indexRel;
int key;
if (stmt->identity_type == REPLICA_IDENTITY_DEFAULT)
{
relation_mark_replica_identity(rel, stmt->identity_type, InvalidOid, true);
return;
}
else if (stmt->identity_type == REPLICA_IDENTITY_FULL)
{
relation_mark_replica_identity(rel, stmt->identity_type, InvalidOid, true);
return;
}
else if (stmt->identity_type == REPLICA_IDENTITY_NOTHING)
{
relation_mark_replica_identity(rel, stmt->identity_type, InvalidOid, true);
return;
}
else if (stmt->identity_type == REPLICA_IDENTITY_INDEX)
{
/* fallthrough */ ;
}
else
elog(ERROR, "unexpected identity type %u", stmt->identity_type);
/* Check that the index exists */
indexOid = get_relname_relid(stmt->name, rel->rd_rel->relnamespace);
if (!OidIsValid(indexOid))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("index \"%s\" for table \"%s\" does not exist",
stmt->name, RelationGetRelationName(rel))));
indexRel = index_open(indexOid, ShareLock);
/* Check that the index is on the relation we're altering. */
if (indexRel->rd_index == NULL ||
indexRel->rd_index->indrelid != RelationGetRelid(rel))
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not an index for table \"%s\"",
RelationGetRelationName(indexRel),
RelationGetRelationName(rel))));
/* The AM must support uniqueness, and the index must in fact be unique. */
if (!indexRel->rd_indam->amcanunique ||
!indexRel->rd_index->indisunique)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot use non-unique index \"%s\" as replica identity",
RelationGetRelationName(indexRel))));
/* Deferred indexes are not guaranteed to be always unique. */
if (!indexRel->rd_index->indimmediate)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot use non-immediate index \"%s\" as replica identity",
RelationGetRelationName(indexRel))));
/* Expression indexes aren't supported. */
if (RelationGetIndexExpressions(indexRel) != NIL)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot use expression index \"%s\" as replica identity",
RelationGetRelationName(indexRel))));
/* Predicate indexes aren't supported. */
if (RelationGetIndexPredicate(indexRel) != NIL)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot use partial index \"%s\" as replica identity",
RelationGetRelationName(indexRel))));
/* Check index for nullable columns. */
for (key = 0; key < IndexRelationGetNumberOfKeyAttributes(indexRel); key++)
{
int16 attno = indexRel->rd_index->indkey.values[key];
Form_pg_attribute attr;
/*
* Reject any other system columns. (Going forward, we'll disallow
* indexes containing such columns in the first place, but they might
* exist in older branches.)
*/
if (attno <= 0)
ereport(ERROR,
(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
errmsg("index \"%s\" cannot be used as replica identity because column %d is a system column",
RelationGetRelationName(indexRel), attno)));
attr = TupleDescAttr(rel->rd_att, attno - 1);
if (!attr->attnotnull)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("index \"%s\" cannot be used as replica identity because column \"%s\" is nullable",
RelationGetRelationName(indexRel),
NameStr(attr->attname))));
}
/* This index is suitable for use as a replica identity. Mark it. */
relation_mark_replica_identity(rel, stmt->identity_type, indexOid, true);
index_close(indexRel, NoLock);
}
/*
* ALTER TABLE ENABLE/DISABLE ROW LEVEL SECURITY
*/
static void
ATExecSetRowSecurity(Relation rel, bool rls)
{
Relation pg_class;
Oid relid;
HeapTuple tuple;
relid = RelationGetRelid(rel);
/* Pull the record for this relation and update it */
pg_class = table_open(RelationRelationId, RowExclusiveLock);
tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for relation %u", relid);
((Form_pg_class) GETSTRUCT(tuple))->relrowsecurity = rls;
CatalogTupleUpdate(pg_class, &tuple->t_self, tuple);
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel), 0);
table_close(pg_class, RowExclusiveLock);
heap_freetuple(tuple);
}
/*
* ALTER TABLE FORCE/NO FORCE ROW LEVEL SECURITY
*/
static void
ATExecForceNoForceRowSecurity(Relation rel, bool force_rls)
{
Relation pg_class;
Oid relid;
HeapTuple tuple;
relid = RelationGetRelid(rel);
pg_class = table_open(RelationRelationId, RowExclusiveLock);
tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for relation %u", relid);
((Form_pg_class) GETSTRUCT(tuple))->relforcerowsecurity = force_rls;
CatalogTupleUpdate(pg_class, &tuple->t_self, tuple);
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel), 0);
table_close(pg_class, RowExclusiveLock);
heap_freetuple(tuple);
}
/*
* ALTER FOREIGN TABLE <name> OPTIONS (...)
*/
static void
ATExecGenericOptions(Relation rel, List *options)
{
Relation ftrel;
ForeignServer *server;
ForeignDataWrapper *fdw;
HeapTuple tuple;
bool isnull;
Datum repl_val[Natts_pg_foreign_table];
bool repl_null[Natts_pg_foreign_table];
bool repl_repl[Natts_pg_foreign_table];
Datum datum;
Form_pg_foreign_table tableform;
if (options == NIL)
return;
ftrel = table_open(ForeignTableRelationId, RowExclusiveLock);
tuple = SearchSysCacheCopy1(FOREIGNTABLEREL,
ObjectIdGetDatum(rel->rd_id));
if (!HeapTupleIsValid(tuple))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("foreign table \"%s\" does not exist",
RelationGetRelationName(rel))));
tableform = (Form_pg_foreign_table) GETSTRUCT(tuple);
server = GetForeignServer(tableform->ftserver);
fdw = GetForeignDataWrapper(server->fdwid);
memset(repl_val, 0, sizeof(repl_val));
memset(repl_null, false, sizeof(repl_null));
memset(repl_repl, false, sizeof(repl_repl));
/* Extract the current options */
datum = SysCacheGetAttr(FOREIGNTABLEREL,
tuple,
Anum_pg_foreign_table_ftoptions,
&isnull);
if (isnull)
datum = PointerGetDatum(NULL);
/* Transform the options */
datum = transformGenericOptions(ForeignTableRelationId,
datum,
options,
fdw->fdwvalidator);
if (PointerIsValid(DatumGetPointer(datum)))
repl_val[Anum_pg_foreign_table_ftoptions - 1] = datum;
else
repl_null[Anum_pg_foreign_table_ftoptions - 1] = true;
repl_repl[Anum_pg_foreign_table_ftoptions - 1] = true;
/* Everything looks good - update the tuple */
tuple = heap_modify_tuple(tuple, RelationGetDescr(ftrel),
repl_val, repl_null, repl_repl);
CatalogTupleUpdate(ftrel, &tuple->t_self, tuple);
/*
* Invalidate relcache so that all sessions will refresh any cached plans
* that might depend on the old options.
*/
CacheInvalidateRelcache(rel);
InvokeObjectPostAlterHook(ForeignTableRelationId,
RelationGetRelid(rel), 0);
table_close(ftrel, RowExclusiveLock);
heap_freetuple(tuple);
}
/*
* ALTER TABLE ALTER COLUMN SET COMPRESSION
*
* Return value is the address of the modified column
*/
static ObjectAddress
ATExecSetCompression(Relation rel,
const char *column,
Node *newValue,
LOCKMODE lockmode)
{
Relation attrel;
HeapTuple tuple;
Form_pg_attribute atttableform;
AttrNumber attnum;
char *compression;
char cmethod;
ObjectAddress address;
compression = strVal(newValue);
attrel = table_open(AttributeRelationId, RowExclusiveLock);
/* copy the cache entry so we can scribble on it below */
tuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), column);
if (!HeapTupleIsValid(tuple))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_COLUMN),
errmsg("column \"%s\" of relation \"%s\" does not exist",
column, RelationGetRelationName(rel))));
/* prevent them from altering a system attribute */
atttableform = (Form_pg_attribute) GETSTRUCT(tuple);
attnum = atttableform->attnum;
if (attnum <= 0)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot alter system column \"%s\"", column)));
/*
* Check that column type is compressible, then get the attribute
* compression method code
*/
cmethod = GetAttributeCompression(atttableform->atttypid, compression);
/* update pg_attribute entry */
atttableform->attcompression = cmethod;
CatalogTupleUpdate(attrel, &tuple->t_self, tuple);
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel),
attnum);
/*
* Apply the change to indexes as well (only for simple index columns,
* matching behavior of index.c ConstructTupleDescriptor()).
*/
SetIndexStorageProperties(rel, attrel, attnum,
false, 0,
true, cmethod,
lockmode);
heap_freetuple(tuple);
table_close(attrel, RowExclusiveLock);
/* make changes visible */
CommandCounterIncrement();
ObjectAddressSubSet(address, RelationRelationId,
RelationGetRelid(rel), attnum);
return address;
}
/*
* Preparation phase for SET LOGGED/UNLOGGED
*
* This verifies that we're not trying to change a temp table. Also,
* existing foreign key constraints are checked to avoid ending up with
* permanent tables referencing unlogged tables.
*
* Return value is false if the operation is a no-op (in which case the
* checks are skipped), otherwise true.
*/
static bool
ATPrepChangePersistence(Relation rel, bool toLogged)
{
Relation pg_constraint;
HeapTuple tuple;
SysScanDesc scan;
ScanKeyData skey[1];
/*
* Disallow changing status for a temp table. Also verify whether we can
* get away with doing nothing; in such cases we don't need to run the
* checks below, either.
*/
switch (rel->rd_rel->relpersistence)
{
case RELPERSISTENCE_TEMP:
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("cannot change logged status of table \"%s\" because it is temporary",
RelationGetRelationName(rel)),
errtable(rel)));
break;
case RELPERSISTENCE_PERMANENT:
if (toLogged)
/* nothing to do */
return false;
break;
case RELPERSISTENCE_UNLOGGED:
if (!toLogged)
/* nothing to do */
return false;
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.")));
/*
* Check existing foreign key constraints to preserve the invariant that
* permanent tables cannot reference unlogged ones. Self-referencing
* foreign keys can safely be ignored.
*/
pg_constraint = table_open(ConstraintRelationId, AccessShareLock);
/*
* Scan conrelid if changing to permanent, else confrelid. This also
* determines whether a useful index exists.
*/
ScanKeyInit(&skey[0],
toLogged ? Anum_pg_constraint_conrelid :
Anum_pg_constraint_confrelid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(RelationGetRelid(rel)));
scan = systable_beginscan(pg_constraint,
toLogged ? ConstraintRelidTypidNameIndexId : InvalidOid,
true, NULL, 1, skey);
while (HeapTupleIsValid(tuple = systable_getnext(scan)))
{
Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(tuple);
if (con->contype == CONSTRAINT_FOREIGN)
{
Oid foreignrelid;
Relation foreignrel;
/* the opposite end of what we used as scankey */
foreignrelid = toLogged ? con->confrelid : con->conrelid;
/* ignore if self-referencing */
if (RelationGetRelid(rel) == foreignrelid)
continue;
foreignrel = relation_open(foreignrelid, AccessShareLock);
if (toLogged)
{
if (!RelationIsPermanent(foreignrel))
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("could not change table \"%s\" to logged because it references unlogged table \"%s\"",
RelationGetRelationName(rel),
RelationGetRelationName(foreignrel)),
errtableconstraint(rel, NameStr(con->conname))));
}
else
{
if (RelationIsPermanent(foreignrel))
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("could not change table \"%s\" to unlogged because it references logged table \"%s\"",
RelationGetRelationName(rel),
RelationGetRelationName(foreignrel)),
errtableconstraint(rel, NameStr(con->conname))));
}
relation_close(foreignrel, AccessShareLock);
}
}
systable_endscan(scan);
table_close(pg_constraint, AccessShareLock);
return true;
}
/*
* Execute ALTER TABLE SET SCHEMA
*/
ObjectAddress
AlterTableNamespace(AlterObjectSchemaStmt *stmt, Oid *oldschema)
{
Relation rel;
Oid relid;
Oid oldNspOid;
Oid nspOid;
RangeVar *newrv;
ObjectAddresses *objsMoved;
ObjectAddress myself;
relid = RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock,
stmt->missing_ok ? RVR_MISSING_OK : 0,
RangeVarCallbackForAlterRelation,
(void *) stmt);
if (!OidIsValid(relid))
{
ereport(NOTICE,
(errmsg("relation \"%s\" does not exist, skipping",
stmt->relation->relname)));
return InvalidObjectAddress;
}
rel = relation_open(relid, NoLock);
oldNspOid = RelationGetNamespace(rel);
/* If it's an owned sequence, disallow moving it by itself. */
if (rel->rd_rel->relkind == RELKIND_SEQUENCE)
{
Oid tableId;
int32 colId;
if (sequenceIsOwned(relid, DEPENDENCY_AUTO, &tableId, &colId) ||
sequenceIsOwned(relid, DEPENDENCY_INTERNAL, &tableId, &colId))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot move an owned sequence into another schema"),
errdetail("Sequence \"%s\" is linked to table \"%s\".",
RelationGetRelationName(rel),
get_rel_name(tableId))));
}
/* Get and lock schema OID and check its permissions. */
newrv = makeRangeVar(stmt->newschema, RelationGetRelationName(rel), -1);
nspOid = RangeVarGetAndCheckCreationNamespace(newrv, NoLock, NULL);
/* common checks on switching namespaces */
CheckSetNamespace(oldNspOid, nspOid);
objsMoved = new_object_addresses();
AlterTableNamespaceInternal(rel, oldNspOid, nspOid, objsMoved);
free_object_addresses(objsMoved);
ObjectAddressSet(myself, RelationRelationId, relid);
if (oldschema)
*oldschema = oldNspOid;
/* close rel, but keep lock until commit */
relation_close(rel, NoLock);
return myself;
}
/*
* The guts of relocating a table or materialized view to another namespace:
* besides moving the relation itself, its dependent objects are relocated to
* the new schema.
*/
void
AlterTableNamespaceInternal(Relation rel, Oid oldNspOid, Oid nspOid,
ObjectAddresses *objsMoved)
{
Relation classRel;
Assert(objsMoved != NULL);
/* OK, modify the pg_class row and pg_depend entry */
classRel = table_open(RelationRelationId, RowExclusiveLock);
AlterRelationNamespaceInternal(classRel, RelationGetRelid(rel), oldNspOid,
nspOid, true, objsMoved);
/* Fix the table's row type too, if it has one */
if (OidIsValid(rel->rd_rel->reltype))
AlterTypeNamespaceInternal(rel->rd_rel->reltype,
nspOid, false, false, objsMoved);
/* Fix other dependent stuff */
AlterIndexNamespaces(classRel, rel, oldNspOid, nspOid, objsMoved);
AlterSeqNamespaces(classRel, rel, oldNspOid, nspOid,
objsMoved, AccessExclusiveLock);
AlterConstraintNamespaces(RelationGetRelid(rel), oldNspOid, nspOid,
false, objsMoved);
table_close(classRel, RowExclusiveLock);
}
/*
* The guts of relocating a relation to another namespace: fix the pg_class
* entry, and the pg_depend entry if any. Caller must already have
* opened and write-locked pg_class.
*/
void
AlterRelationNamespaceInternal(Relation classRel, Oid relOid,
Oid oldNspOid, Oid newNspOid,
bool hasDependEntry,
ObjectAddresses *objsMoved)
{
HeapTuple classTup;
Form_pg_class classForm;
ObjectAddress thisobj;
bool already_done = false;
classTup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relOid));
if (!HeapTupleIsValid(classTup))
elog(ERROR, "cache lookup failed for relation %u", relOid);
classForm = (Form_pg_class) GETSTRUCT(classTup);
Assert(classForm->relnamespace == oldNspOid);
thisobj.classId = RelationRelationId;
thisobj.objectId = relOid;
thisobj.objectSubId = 0;
/*
* If the object has already been moved, don't move it again. If it's
* already in the right place, don't move it, but still fire the object
* access hook.
*/
already_done = object_address_present(&thisobj, objsMoved);
if (!already_done && oldNspOid != newNspOid)
{
/* 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, &classTup->t_self, classTup);
/* 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));
}
if (!already_done)
{
add_exact_object_address(&thisobj, objsMoved);
InvokeObjectPostAlterHook(RelationRelationId, relOid, 0);
}
heap_freetuple(classTup);
}
/*
* Move all indexes for the specified relation to another namespace.
*
* Note: we assume adequate permission checking was done by the caller,
* and that the caller has a suitable lock on the owning relation.
*/
static void
AlterIndexNamespaces(Relation classRel, Relation rel,
Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved)
{
List *indexList;
ListCell *l;
indexList = RelationGetIndexList(rel);
foreach(l, indexList)
{
Oid indexOid = lfirst_oid(l);
ObjectAddress thisobj;
thisobj.classId = RelationRelationId;
thisobj.objectId = indexOid;
thisobj.objectSubId = 0;
/*
* Note: currently, the index will not have its own dependency on the
* namespace, so we don't need to do changeDependencyFor(). There's no
* row type in pg_type, either.
*
* XXX this objsMoved test may be pointless -- surely we have a single
* dependency link from a relation to each index?
*/
if (!object_address_present(&thisobj, objsMoved))
{
AlterRelationNamespaceInternal(classRel, indexOid,
oldNspOid, newNspOid,
false, objsMoved);
add_exact_object_address(&thisobj, objsMoved);
}
}
list_free(indexList);
}
/*
* Move all identity and SERIAL-column sequences of the specified relation to another
* namespace.
*
* Note: we assume adequate permission checking was done by the caller,
* and that the caller has a suitable lock on the owning relation.
*/
static void
AlterSeqNamespaces(Relation classRel, Relation rel,
Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved,
LOCKMODE lockmode)
{
Relation depRel;
SysScanDesc scan;
ScanKeyData key[2];
HeapTuple tup;
/*
* SERIAL sequences are those having an auto dependency on one of the
* table's columns (we don't care *which* column, exactly).
*/
depRel = table_open(DependRelationId, AccessShareLock);
ScanKeyInit(&key[0],
Anum_pg_depend_refclassid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(RelationRelationId));
ScanKeyInit(&key[1],
Anum_pg_depend_refobjid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(RelationGetRelid(rel)));
/* we leave refobjsubid unspecified */
scan = systable_beginscan(depRel, DependReferenceIndexId, true,
NULL, 2, key);
while (HeapTupleIsValid(tup = systable_getnext(scan)))
{
Form_pg_depend depForm = (Form_pg_depend) GETSTRUCT(tup);
Relation seqRel;
/* skip dependencies other than auto dependencies on columns */
if (depForm->refobjsubid == 0 ||
depForm->classid != RelationRelationId ||
depForm->objsubid != 0 ||
!(depForm->deptype == DEPENDENCY_AUTO || depForm->deptype == DEPENDENCY_INTERNAL))
continue;
/* Use relation_open just in case it's an index */
seqRel = relation_open(depForm->objid, lockmode);
/* skip non-sequence relations */
if (RelationGetForm(seqRel)->relkind != RELKIND_SEQUENCE)
{
/* No need to keep the lock */
relation_close(seqRel, lockmode);
continue;
}
/* Fix the pg_class and pg_depend entries */
AlterRelationNamespaceInternal(classRel, depForm->objid,
oldNspOid, newNspOid,
true, objsMoved);
/*
* Sequences used to have entries in pg_type, but no longer do. If we
* ever re-instate that, we'll need to move the pg_type entry to the
* new namespace, too (using AlterTypeNamespaceInternal).
*/
Assert(RelationGetForm(seqRel)->reltype == InvalidOid);
/* Now we can close it. Keep the lock till end of transaction. */
relation_close(seqRel, NoLock);
}
systable_endscan(scan);
relation_close(depRel, AccessShareLock);
}
/*
* This code supports
* CREATE TEMP TABLE ... ON COMMIT { DROP | PRESERVE ROWS | DELETE ROWS }
*
* Because we only support this for TEMP tables, it's sufficient to remember
* the state in a backend-local data structure.
*/
/*
* Register a newly-created relation's ON COMMIT action.
*/
void
register_on_commit_action(Oid relid, OnCommitAction action)
{
OnCommitItem *oc;
MemoryContext oldcxt;
/*
* We needn't bother registering the relation unless there is an ON COMMIT
* action we need to take.
*/
if (action == ONCOMMIT_NOOP || action == ONCOMMIT_PRESERVE_ROWS)
return;
oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
oc = (OnCommitItem *) palloc(sizeof(OnCommitItem));
oc->relid = relid;
oc->oncommit = action;
oc->creating_subid = GetCurrentSubTransactionId();
oc->deleting_subid = InvalidSubTransactionId;
/*
* We use lcons() here so that ON COMMIT actions are processed in reverse
* order of registration. That might not be essential but it seems
* reasonable.
*/
on_commits = lcons(oc, on_commits);
MemoryContextSwitchTo(oldcxt);
}
/*
* Unregister any ON COMMIT action when a relation is deleted.
*
* Actually, we only mark the OnCommitItem entry as to be deleted after commit.
*/
void
remove_on_commit_action(Oid relid)
{
ListCell *l;
foreach(l, on_commits)
{
OnCommitItem *oc = (OnCommitItem *) lfirst(l);
if (oc->relid == relid)
{
oc->deleting_subid = GetCurrentSubTransactionId();
break;
}
}
}
/*
* Perform ON COMMIT actions.
*
* This is invoked just before actually committing, since it's possible
* to encounter errors.
*/
void
PreCommit_on_commit_actions(void)
{
ListCell *l;
List *oids_to_truncate = NIL;
List *oids_to_drop = NIL;
foreach(l, on_commits)
{
OnCommitItem *oc = (OnCommitItem *) lfirst(l);
/* Ignore entry if already dropped in this xact */
if (oc->deleting_subid != InvalidSubTransactionId)
continue;
switch (oc->oncommit)
{
case ONCOMMIT_NOOP:
case ONCOMMIT_PRESERVE_ROWS:
/* Do nothing (there shouldn't be such entries, actually) */
break;
case ONCOMMIT_DELETE_ROWS:
/*
* If this transaction hasn't accessed any temporary
* relations, we can skip truncating ON COMMIT DELETE ROWS
* tables, as they must still be empty.
*/
if ((MyXactFlags & XACT_FLAGS_ACCESSEDTEMPNAMESPACE))
oids_to_truncate = lappend_oid(oids_to_truncate, oc->relid);
break;
case ONCOMMIT_DROP:
oids_to_drop = lappend_oid(oids_to_drop, oc->relid);
break;
}
}
/*
* Truncate relations before dropping so that all dependencies between
* relations are removed after they are worked on. Doing it like this
* might be a waste as it is possible that a relation being truncated will
* be dropped anyway due to its parent being dropped, but this makes the
* code more robust because of not having to re-check that the relation
* exists at truncation time.
*/
if (oids_to_truncate != NIL)
heap_truncate(oids_to_truncate);
if (oids_to_drop != NIL)
{
ObjectAddresses *targetObjects = new_object_addresses();
foreach(l, oids_to_drop)
{
ObjectAddress object;
object.classId = RelationRelationId;
object.objectId = lfirst_oid(l);
object.objectSubId = 0;
Assert(!object_address_present(&object, targetObjects));
add_exact_object_address(&object, targetObjects);
}
/*
* Object deletion might involve toast table access (to clean up
* toasted catalog entries), so ensure we have a valid snapshot.
*/
PushActiveSnapshot(GetTransactionSnapshot());
/*
* Since this is an automatic drop, rather than one directly initiated
* by the user, we pass the PERFORM_DELETION_INTERNAL flag.
*/
performMultipleDeletions(targetObjects, DROP_CASCADE,
PERFORM_DELETION_INTERNAL | PERFORM_DELETION_QUIETLY);
PopActiveSnapshot();
#ifdef USE_ASSERT_CHECKING
/*
* Note that table deletion will call remove_on_commit_action, so the
* entry should get marked as deleted.
*/
foreach(l, on_commits)
{
OnCommitItem *oc = (OnCommitItem *) lfirst(l);
if (oc->oncommit != ONCOMMIT_DROP)
continue;
Assert(oc->deleting_subid != InvalidSubTransactionId);
}
#endif
}
}
/*
* Post-commit or post-abort cleanup for ON COMMIT management.
*
* All we do here is remove no-longer-needed OnCommitItem entries.
*
* During commit, remove entries that were deleted during this transaction;
* during abort, remove those created during this transaction.
*/
void
AtEOXact_on_commit_actions(bool isCommit)
{
ListCell *cur_item;
foreach(cur_item, on_commits)
{
OnCommitItem *oc = (OnCommitItem *) lfirst(cur_item);
if (isCommit ? oc->deleting_subid != InvalidSubTransactionId :
oc->creating_subid != InvalidSubTransactionId)
{
/* cur_item must be removed */
on_commits = foreach_delete_current(on_commits, cur_item);
pfree(oc);
}
else
{
/* cur_item must be preserved */
oc->creating_subid = InvalidSubTransactionId;
oc->deleting_subid = InvalidSubTransactionId;
}
}
}
/*
* Post-subcommit or post-subabort cleanup for ON COMMIT management.
*
* During subabort, we can immediately remove entries created during this
* subtransaction. During subcommit, just relabel entries marked during
* this subtransaction as being the parent's responsibility.
*/
void
AtEOSubXact_on_commit_actions(bool isCommit, SubTransactionId mySubid,
SubTransactionId parentSubid)
{
ListCell *cur_item;
foreach(cur_item, on_commits)
{
OnCommitItem *oc = (OnCommitItem *) lfirst(cur_item);
if (!isCommit && oc->creating_subid == mySubid)
{
/* cur_item must be removed */
on_commits = foreach_delete_current(on_commits, cur_item);
pfree(oc);
}
else
{
/* cur_item must be preserved */
if (oc->creating_subid == mySubid)
oc->creating_subid = parentSubid;
if (oc->deleting_subid == mySubid)
oc->deleting_subid = isCommit ? parentSubid : InvalidSubTransactionId;
}
}
}
/*
* This is intended as a callback for RangeVarGetRelidExtended(). It allows
* the relation to be locked only if (1) it's a plain or partitioned table,
* materialized view, or TOAST table and (2) the current user is the owner (or
* the superuser) or has been granted MAINTAIN. This meets the
* permission-checking needs of CLUSTER, REINDEX TABLE, and REFRESH
* MATERIALIZED VIEW; we expose it here so that it can be used by all.
*/
void
RangeVarCallbackMaintainsTable(const RangeVar *relation,
Oid relId, Oid oldRelId, void *arg)
{
char relkind;
AclResult aclresult;
/* Nothing to do if the relation was not found. */
if (!OidIsValid(relId))
return;
/*
* If the relation does exist, check whether it's an index. But note that
* the relation might have been dropped between the time we did the name
* lookup and now. In that case, there's nothing to do.
*/
relkind = get_rel_relkind(relId);
if (!relkind)
return;
if (relkind != RELKIND_RELATION && relkind != RELKIND_TOASTVALUE &&
relkind != RELKIND_MATVIEW && relkind != RELKIND_PARTITIONED_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not a table or materialized view", relation->relname)));
/* Check permissions */
aclresult = pg_class_aclcheck(relId, GetUserId(), ACL_MAINTAIN);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult,
get_relkind_objtype(get_rel_relkind(relId)),
relation->relname);
}
/*
* Callback to RangeVarGetRelidExtended() for TRUNCATE processing.
*/
static void
RangeVarCallbackForTruncate(const RangeVar *relation,
Oid relId, Oid oldRelId, void *arg)
{
HeapTuple tuple;
/* Nothing to do if the relation was not found. */
if (!OidIsValid(relId))
return;
tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relId));
if (!HeapTupleIsValid(tuple)) /* should not happen */
elog(ERROR, "cache lookup failed for relation %u", relId);
truncate_check_rel(relId, (Form_pg_class) GETSTRUCT(tuple));
truncate_check_perms(relId, (Form_pg_class) GETSTRUCT(tuple));
ReleaseSysCache(tuple);
}
/*
* 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;
/* Nothing to do if the relation was not found. */
if (!OidIsValid(relId))
return;
tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relId));
if (!HeapTupleIsValid(tuple)) /* should not happen */
elog(ERROR, "cache lookup failed for relation %u", relId);
if (!object_ownercheck(RelationRelationId, relId, GetUserId()))
aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relId)),
relation->relname);
if (!allowSystemTableMods &&
IsSystemClass(relId, (Form_pg_class) GETSTRUCT(tuple)))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied: \"%s\" is a system catalog",
relation->relname)));
ReleaseSysCache(tuple);
}
/*
* Common RangeVarGetRelid callback for rename, set schema, and alter table
* processing.
*/
static void
RangeVarCallbackForAlterRelation(const RangeVar *rv, Oid relid, Oid oldrelid,
void *arg)
{
Node *stmt = (Node *) arg;
ObjectType reltype;
HeapTuple tuple;
Form_pg_class classform;
AclResult aclresult;
char relkind;
tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
if (!HeapTupleIsValid(tuple))
return; /* concurrently dropped */
classform = (Form_pg_class) GETSTRUCT(tuple);
relkind = classform->relkind;
/* Must own relation. */
if (!object_ownercheck(RelationRelationId, relid, GetUserId()))
aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relid)), rv->relname);
/* No system table modifications unless explicitly allowed. */
if (!allowSystemTableMods && IsSystemClass(relid, classform))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied: \"%s\" is a system catalog",
rv->relname)));
/*
* 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 */
}
/*
* 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)));
if (reltype == OBJECT_VIEW && relkind != RELKIND_VIEW)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not a view", rv->relname)));
if (reltype == OBJECT_MATVIEW && relkind != RELKIND_MATVIEW)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not a materialized view", rv->relname)));
if (reltype == OBJECT_FOREIGN_TABLE && relkind != RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not a foreign table", rv->relname)));
if (reltype == OBJECT_TYPE && relkind != RELKIND_COMPOSITE_TYPE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not a composite type", rv->relname)));
if (reltype == OBJECT_INDEX && relkind != RELKIND_INDEX &&
relkind != RELKIND_PARTITIONED_INDEX
&& !IsA(stmt, RenameStmt))
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not an index", rv->relname)));
/*
* Don't allow ALTER TABLE on composite types. We want people to use ALTER
* TYPE for that.
*/
if (reltype != OBJECT_TYPE && relkind == RELKIND_COMPOSITE_TYPE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is a composite type", rv->relname),
/* translator: %s is an SQL ALTER command */
errhint("Use %s instead.",
"ALTER TYPE")));
/*
* Don't allow ALTER TABLE .. SET SCHEMA on relations that can't be moved
* to a different schema, such as indexes and TOAST tables.
*/
if (IsA(stmt, AlterObjectSchemaStmt))
{
if (relkind == RELKIND_INDEX || relkind == RELKIND_PARTITIONED_INDEX)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot change schema of index \"%s\"",
rv->relname),
errhint("Change the schema of the table instead.")));
else if (relkind == RELKIND_COMPOSITE_TYPE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot change schema of composite type \"%s\"",
rv->relname),
/* translator: %s is an SQL ALTER command */
errhint("Use %s instead.",
"ALTER TYPE")));
else if (relkind == RELKIND_TOASTVALUE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot change schema of TOAST table \"%s\"",
rv->relname),
errhint("Change the schema of the table instead.")));
}
ReleaseSysCache(tuple);
}
/*
* Transform any expressions present in the partition key
*
* Returns a transformed PartitionSpec.
*/
static PartitionSpec *
transformPartitionSpec(Relation rel, PartitionSpec *partspec)
{
PartitionSpec *newspec;
ParseState *pstate;
ParseNamespaceItem *nsitem;
ListCell *l;
newspec = makeNode(PartitionSpec);
newspec->strategy = partspec->strategy;
newspec->partParams = NIL;
newspec->location = partspec->location;
/* Check valid number of columns for strategy */
if (partspec->strategy == PARTITION_STRATEGY_LIST &&
list_length(partspec->partParams) != 1)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("cannot use \"list\" partition strategy with more than one column")));
/*
* Create a dummy ParseState and insert the target relation as its sole
* rangetable entry. We need a ParseState for transformExpr.
*/
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)
{
PartitionElem *pelem = lfirst_node(PartitionElem, l);
if (pelem->expr)
{
/* Copy, to avoid scribbling on the input */
pelem = copyObject(pelem);
/* Now do parse transformation of the expression */
pelem->expr = transformExpr(pstate, pelem->expr,
EXPR_KIND_PARTITION_EXPRESSION);
/* we have to fix its collations too */
assign_expr_collations(pstate, pelem->expr);
}
newspec->partParams = lappend(newspec->partParams, pelem);
}
return newspec;
}
/*
* Compute per-partition-column information from a list of PartitionElems.
* Expressions in the PartitionElems must be parse-analyzed already.
*/
static void
ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNumber *partattrs,
List **partexprs, Oid *partopclass, Oid *partcollation,
PartitionStrategy strategy)
{
int attn;
ListCell *lc;
Oid am_oid;
attn = 0;
foreach(lc, partParams)
{
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)));
/*
* Generated columns cannot work: They are computed after BEFORE
* triggers, but partition routing is done before all triggers.
*/
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];
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;
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
{
Bitmapset *expr_attrs = NULL;
int i;
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.
*/
/*
* 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.
*/
pull_varattnos(expr, 1, &expr_attrs);
for (i = FirstLowInvalidHeapAttributeNumber; i < 0; i++)
{
if (bms_is_member(i - FirstLowInvalidHeapAttributeNumber,
expr_attrs))
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("partition key expressions cannot contain system column references")));
}
/*
* Generated columns cannot work: They are computed after
* BEFORE triggers, but partition routing is done before all
* triggers.
*/
i = -1;
while ((i = bms_next_member(expr_attrs, i)) >= 0)
{
AttrNumber attno = i + FirstLowInvalidHeapAttributeNumber;
if (attno > 0 &&
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)));
}
/*
* 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);
/*
* 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")));
/*
* 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")));
}
}
/*
* Apply collation override if any
*/
if (pelem->collation)
attcollation = get_collation_oid(pelem->collation, false);
/*
* 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))));
}
partcollation[attn] = attcollation;
/*
* Identify the appropriate operator class. For list and range
* partitioning, we use a btree operator class; hash partitioning uses
* a hash operator class.
*/
if (strategy == PARTITION_STRATEGY_HASH)
am_oid = HASH_AM_OID;
else
am_oid = BTREE_AM_OID;
if (!pelem->opclass)
{
partopclass[attn] = GetDefaultOpClass(atttype, am_oid);
if (!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);
attn++;
}
}
/*
* PartConstraintImpliedByRelConstraint
* Do scanrel's existing constraints imply the partition constraint?
*
* "Existing constraints" include its check constraints and column-level
* not-null constraints. partConstraint describes the partition constraint,
* in implicit-AND form.
*/
bool
PartConstraintImpliedByRelConstraint(Relation scanrel,
List *partConstraint)
{
List *existConstraint = NIL;
TupleConstr *constr = RelationGetDescr(scanrel)->constr;
int i;
if (constr && constr->has_not_null)
{
int natts = scanrel->rd_att->natts;
for (i = 1; i <= natts; i++)
{
Form_pg_attribute att = TupleDescAttr(scanrel->rd_att, i - 1);
if (att->attnotnull && !att->attisdropped)
{
NullTest *ntest = makeNode(NullTest);
ntest->arg = (Expr *) makeVar(1,
i,
att->atttypid,
att->atttypmod,
att->attcollation,
0);
ntest->nulltesttype = IS_NOT_NULL;
/*
* 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);
}
}
}
return ConstraintImpliedByRelConstraint(scanrel, partConstraint, existConstraint);
}
/*
* ConstraintImpliedByRelConstraint
* Do scanrel's existing constraints imply the given constraint?
*
* testConstraint is the constraint to validate. provenConstraint is a
* caller-provided list of conditions which this function may assume
* to be true. Both provenConstraint and testConstraint must be in
* implicit-AND form, must only contain immutable clauses, and must
* contain only Vars with varno = 1.
*/
bool
ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint, List *provenConstraint)
{
List *existConstraint = list_copy(provenConstraint);
TupleConstr *constr = RelationGetDescr(scanrel)->constr;
int num_check,
i;
num_check = (constr != NULL) ? constr->num_check : 0;
for (i = 0; i < num_check; i++)
{
Node *cexpr;
/*
* If this constraint hasn't been fully validated yet, we must ignore
* it here.
*/
if (!constr->check[i].ccvalid)
continue;
cexpr = stringToNode(constr->check[i].ccbin);
/*
* 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);
existConstraint = list_concat(existConstraint,
make_ands_implicit((Expr *) cexpr));
}
/*
* 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);
}
/*
* QueuePartitionConstraintValidation
*
* Add an entry to wqueue to have the given partition constraint validated by
* Phase 3, for the given relation, and all its children.
*
* We first verify whether the given constraint is implied by pre-existing
* relation constraints; if it is, there's no need to scan the table to
* validate, so don't queue in that case.
*/
static void
QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
List *partConstraint,
bool validate_default)
{
/*
* 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))));
return;
}
/*
* Constraints proved insufficient. For plain relations, queue a
* validation item now; for partitioned tables, recurse to process each
* partition.
*/
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;
for (i = 0; i < partdesc->nparts; i++)
{
Relation part_rel;
List *thisPartConstraint;
/*
* This is the minimum lock we need to prevent deadlocks.
*/
part_rel = table_open(partdesc->oids[i], AccessExclusiveLock);
/*
* 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 */
}
}
}
/*
* 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)
{
/* OK to create inheritance. Rest of the checks performed there */
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);
}
/*
* ALTER TABLE <name> ATTACH PARTITION <partition-name> FOR VALUES
*
* Return the address of the newly attached partition.
*/
static ObjectAddress
ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
AlterTableUtilityContext *context)
{
Relation attachrel,
catalog;
List *attachrel_children;
List *partConstraint;
SysScanDesc scan;
ScanKeyData skey;
AttrNumber attno;
int natts;
TupleDesc tupleDesc;
ObjectAddress address;
const char *trigger_name;
Oid defaultPartOid;
List *partBoundConstraint;
ParseState *pstate = make_parsestate(NULL);
pstate->p_sourcetext = context->queryString;
/*
* 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);
attachrel = table_openrv(cmd->name, AccessExclusiveLock);
/*
* XXX I think it'd be a good idea to grab locks on all tables referenced
* by FKs at this point also.
*/
/*
* Must be owner of both parent and source table -- parent was checked by
* ATSimplePermissions call in ATPrepCmd
*/
ATSimplePermissions(AT_AttachPartition, attachrel, ATT_TABLE | ATT_FOREIGN_TABLE);
/* A partition can only have one parent */
if (attachrel->rd_rel->relispartition)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is already a partition",
RelationGetRelationName(attachrel))));
if (OidIsValid(attachrel->rd_rel->reloftype))
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach a typed table as partition")));
/*
* Table being attached should not already be part of inheritance; either
* as a child table...
*/
catalog = table_open(InheritsRelationId, AccessShareLock);
ScanKeyInit(&skey,
Anum_pg_inherits_inhrelid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(RelationGetRelid(attachrel)));
scan = systable_beginscan(catalog, InheritsRelidSeqnoIndexId, true,
NULL, 1, &skey);
if (HeapTupleIsValid(systable_getnext(scan)))
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach inheritance child as partition")));
systable_endscan(scan);
/* ...or as a parent table (except the case when it is partitioned) */
ScanKeyInit(&skey,
Anum_pg_inherits_inhparent,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(RelationGetRelid(attachrel)));
scan = systable_beginscan(catalog, InheritsParentIndexId, true, NULL,
1, &skey);
if (HeapTupleIsValid(systable_getnext(scan)) &&
attachrel->rd_rel->relkind == RELKIND_RELATION)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach inheritance parent as partition")));
systable_endscan(scan);
table_close(catalog, AccessShareLock);
/*
* Prevent circularity by seeing if rel is a partition of attachrel. (In
* particular, this disallows making a rel a partition of itself.)
*
* We do that by checking if rel is a member of the list of attachrel's
* partitions provided the latter is partitioned at all. We want to avoid
* having to construct this list again, so we request the strongest lock
* on all partitions. We need the strongest lock, because we may decide
* to scan them if we find out that the table being attached (or its leaf
* partitions) may contain rows that violate the partition constraint. If
* the table has a constraint that would prevent such rows, which by
* definition is present in all the partitions, we need not scan the
* table, nor its partitions. But we cannot risk a deadlock by taking a
* weaker lock now and the stronger one only when needed.
*/
attachrel_children = find_all_inheritors(RelationGetRelid(attachrel),
AccessExclusiveLock, NULL);
if (list_member_oid(attachrel_children, RelationGetRelid(rel)))
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_TABLE),
errmsg("circular inheritance not allowed"),
errdetail("\"%s\" is already a child of \"%s\".",
RelationGetRelationName(rel),
RelationGetRelationName(attachrel))));
/* 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))));
/* 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))));
/* If the parent is temp, it must belong to this session */
if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
!rel->rd_islocaltemp)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach as partition of temporary relation of another session")));
/* Ditto for the partition */
if (attachrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
!attachrel->rd_islocaltemp)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach temporary relation of another session as partition")));
/*
* Check if attachrel has any identity columns or any columns that aren't
* in the parent.
*/
tupleDesc = RelationGetDescr(attachrel);
natts = tupleDesc->natts;
for (attno = 1; attno <= natts; attno++)
{
Form_pg_attribute attribute = TupleDescAttr(tupleDesc, attno - 1);
char *attributeName = NameStr(attribute->attname);
/* Ignore dropped */
if (attribute->attisdropped)
continue;
if (attribute->attidentity)
ereport(ERROR,
errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("table \"%s\" being attached contains an identity column \"%s\"",
RelationGetRelationName(attachrel), attributeName),
errdetail("The new partition may not contain an identity column."));
/* Try to find the column in parent (matching on column name) */
if (!SearchSysCacheExists2(ATTNAME,
ObjectIdGetDatum(RelationGetRelid(rel)),
CStringGetDatum(attributeName)))
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("table \"%s\" contains column \"%s\" not found in parent \"%s\"",
RelationGetRelationName(attachrel), attributeName,
RelationGetRelationName(rel)),
errdetail("The new partition may contain only the columns present in parent.")));
}
/*
* 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().
*/
trigger_name = FindTriggerIncompatibleWithInheritance(attachrel->trigdesc);
if (trigger_name != NULL)
ereport(ERROR,
(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.")));
/*
* 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);
/* Attach a new partition to the partitioned table. */
attachPartitionTable(wqueue, rel, attachrel, cmd->bound);
/*
* Generate 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);
partConstraint = list_concat(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);
}
/*
* 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!)
*/
if (OidIsValid(defaultPartOid))
{
Relation defaultrel;
List *defPartConstraint;
Assert(!cmd->bound->is_default);
/* we already hold a lock on the default partition */
defaultrel = table_open(defaultPartOid, NoLock);
defPartConstraint =
get_proposed_default_constraint(partBoundConstraint);
/*
* 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);
/* keep our lock until commit. */
table_close(defaultrel, NoLock);
}
ObjectAddressSet(address, RelationRelationId, RelationGetRelid(attachrel));
/*
* 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;
foreach(l, attachrel_children)
{
CacheInvalidateRelcacheByRelid(lfirst_oid(l));
}
}
/* keep our lock until commit */
table_close(attachrel, NoLock);
return address;
}
/*
* AttachPartitionEnsureIndexes
* subroutine for ATExecAttachPartition to create/match indexes
*
* 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.
*/
static void
AttachPartitionEnsureIndexes(List **wqueue, Relation rel, Relation attachrel)
{
List *idxes;
List *attachRelIdxs;
Relation *attachrelIdxRels;
IndexInfo **attachInfos;
ListCell *cell;
MemoryContext cxt;
MemoryContext oldcxt;
cxt = AllocSetContextCreate(CurrentMemoryContext,
"AttachPartitionEnsureIndexes",
ALLOCSET_DEFAULT_SIZES);
oldcxt = MemoryContextSwitchTo(cxt);
idxes = RelationGetIndexList(rel);
attachRelIdxs = RelationGetIndexList(attachrel);
attachrelIdxRels = palloc(sizeof(Relation) * list_length(attachRelIdxs));
attachInfos = palloc(sizeof(IndexInfo *) * list_length(attachRelIdxs));
/* Build arrays of all existing indexes and their IndexInfos */
foreach(cell, attachRelIdxs)
{
Oid cldIdxId = lfirst_oid(cell);
int i = foreach_current_index(cell);
attachrelIdxRels[i] = index_open(cldIdxId, AccessShareLock);
attachInfos[i] = BuildIndexInfo(attachrelIdxRels[i]);
}
/*
* 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.
*/
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);
}
goto out;
}
/*
* 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)
{
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;
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;
/* Ensure they're both the same type of constraint */
if (get_constraint_type(constraintOid) !=
get_constraint_type(cldConstrOid))
continue;
}
/* bingo. */
IndexSetParentIndex(attachrelIdxRels[i], idx);
if (OidIsValid(constraintOid))
ConstraintSetParentConstraint(cldConstrOid, constraintOid,
RelationGetRelid(attachrel));
found = true;
CommandCounterIncrement();
break;
}
}
/*
* If no suitable index was found in the partition-to-be, create one
* now.
*/
if (!found)
{
IndexStmt *stmt;
Oid conOid;
stmt = generateClonedIndexStmt(NULL,
idxRel, attmap,
&conOid);
/*
* If the index is a primary key, mark all columns as NOT NULL if
* they aren't already.
*/
if (stmt->primary)
{
MemoryContextSwitchTo(oldcxt);
for (int j = 0; j < info->ii_NumIndexKeyAttrs; j++)
{
AttrNumber childattno;
childattno = get_attnum(RelationGetRelid(attachrel),
get_attname(RelationGetRelid(rel),
info->ii_IndexAttrNumbers[j],
false));
set_attnotnull(wqueue, attachrel, childattno,
true, AccessExclusiveLock);
}
MemoryContextSwitchTo(cxt);
}
DefineIndex(RelationGetRelid(attachrel), stmt, InvalidOid,
RelationGetRelid(idxRel),
conOid,
-1,
true, false, false, false, false);
}
index_close(idxRel, AccessShareLock);
}
out:
/* Clean up. */
for (int i = 0; i < list_length(attachRelIdxs); i++)
index_close(attachrelIdxRels[i], AccessShareLock);
MemoryContextSwitchTo(oldcxt);
MemoryContextDelete(cxt);
}
/*
* CloneRowTriggersToPartition
* subroutine for ATExecAttachPartition/DefineRelation to create row
* triggers on partitions
*/
static void
CloneRowTriggersToPartition(Relation parent, Relation partition)
{
Relation pg_trigger;
ScanKeyData key;
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);
perTupCxt = AllocSetContextCreate(CurrentMemoryContext,
"clone trig", ALLOCSET_SMALL_SIZES);
while (HeapTupleIsValid(tuple = systable_getnext(scan)))
{
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;
/*
* Ignore statement-level triggers; those are not cloned.
*/
if (!TRIGGER_FOR_ROW(trigForm->tgtype))
continue;
/*
* Don't clone internal triggers, because the constraint cloning code
* will.
*/
if (trigForm->tgisinternal)
continue;
/*
* 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));
/* Use short-lived context for CREATE TRIGGER */
oldcxt = MemoryContextSwitchTo(perTupCxt);
/*
* 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)
{
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);
}
/*
* 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;
for (i = 0; i < trigForm->tgattr.dim1; i++)
{
Form_pg_attribute col;
col = TupleDescAttr(parent->rd_att,
trigForm->tgattr.values[i] - 1);
cols = lappend(cols,
makeString(pstrdup(NameStr(col->attname))));
}
}
/* Reconstruct trigger arguments list. */
if (trigForm->tgnargs > 0)
{
char *p;
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));
p = (char *) VARDATA_ANY(DatumGetByteaPP(value));
for (int i = 0; i < trigForm->tgnargs; i++)
{
trigargs = lappend(trigargs, makeString(pstrdup(p)));
p += strlen(p) + 1;
}
}
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);
}
MemoryContextDelete(perTupCxt);
systable_endscan(scan);
table_close(pg_trigger, RowExclusiveLock);
}
/*
* 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.
*
* 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).
*/
static ObjectAddress
ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
RangeVar *name, bool concurrent)
{
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);
/*
* 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);
/*
* 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)
{
Oid partrelid,
parentrelid;
LOCKTAG tag;
char *parentrelname;
char *partrelname;
/*
* Add a new constraint to the partition being detached, which
* supplants the partition constraint (unless there is one already).
*/
DetachAddConstraintIfNeeded(wqueue, partRel);
/*
* 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();
StartTransactionCommand();
/*
* 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.
*
* 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.
*/
rel = try_relation_open(parentrelid, ShareUpdateExclusiveLock);
partRel = try_relation_open(partrelid, AccessExclusiveLock);
/* If the relations aren't there, something bad happened; bail out */
if (rel == NULL)
{
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)));
}
if (partRel == NULL)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("partition \"%s\" was removed concurrently", partrelname)));
tab->rel = rel;
}
/* Do the final part of detaching */
DetachPartitionFinalize(rel, partRel, concurrent, defaultPartOid);
ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
/* keep our lock until commit */
table_close(partRel, NoLock);
return address;
}
/*
* Second part of ALTER TABLE .. DETACH.
*
* This is separate so that it can be run independently when the second
* transaction of the concurrent algorithm fails (crash or abort).
*/
static void
DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
Oid defaultPartOid)
{
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;
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));
/*
* Detach any foreign keys that are inherited. This includes creating
* additional action triggers.
*/
fks = copyObject(RelationGetFKeyList(partRel));
if (fks != NIL)
trigrel = table_open(TriggerRelationId, RowExclusiveLock);
foreach(cell, fks)
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
Constraint *fkconstraint;
Oid insertTriggerOid,
updateTriggerOid;
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);
/* consider only the inherited foreign keys */
if (conform->contype != CONSTRAINT_FOREIGN ||
!OidIsValid(conform->conparentid))
{
ReleaseSysCache(contup);
continue;
}
/* unset conparentid and adjust conislocal, coninhcount, etc. */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
/*
* Also, look up the partition's "check" triggers corresponding to the
* constraint being detached and detach them from the parent triggers.
*/
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));
/*
* Make the action triggers on the referenced relation. When this was
* a partition the action triggers pointed to the parent rel (they
* still do), but now we need separate ones of our own.
*/
fkconstraint = makeNode(Constraint);
fkconstraint->contype = CONSTRAINT_FOREIGN;
fkconstraint->conname = pstrdup(NameStr(conform->conname));
fkconstraint->deferrable = conform->condeferrable;
fkconstraint->initdeferred = conform->condeferred;
fkconstraint->location = -1;
fkconstraint->pktable = NULL;
fkconstraint->fk_attrs = NIL;
fkconstraint->pk_attrs = NIL;
fkconstraint->fk_matchtype = conform->confmatchtype;
fkconstraint->fk_upd_action = conform->confupdtype;
fkconstraint->fk_del_action = conform->confdeltype;
fkconstraint->fk_del_set_cols = NIL;
fkconstraint->old_conpfeqop = NIL;
fkconstraint->old_pktable_oid = InvalidOid;
fkconstraint->skip_validation = false;
fkconstraint->initially_valid = true;
createForeignKeyActionTriggers(partRel, conform->confrelid,
fkconstraint, fk->conoid,
conform->conindid,
InvalidOid, InvalidOid,
NULL, NULL);
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();
ObjectAddressSet(constraint, ConstraintRelationId, constrOid);
performDeletion(&constraint, DROP_RESTRICT, 0);
}
/* Now we can detach indexes */
indexes = RelationGetIndexList(partRel);
foreach(cell, indexes)
{
Oid idxid = lfirst_oid(cell);
Relation idx;
Oid constrOid;
if (!has_superclass(idxid))
continue;
Assert((IndexGetRelation(get_partition_parent(idxid, false), false) ==
RelationGetRelid(rel)));
idx = index_open(idxid, AccessExclusiveLock);
IndexSetParentIndex(idx, InvalidOid);
/* If there's a constraint associated with the index, detach it too */
constrOid = get_relation_idx_constraint_oid(RelationGetRelid(partRel),
idxid);
if (OidIsValid(constrOid))
ConstraintSetParentConstraint(constrOid, InvalidOid, InvalidOid);
index_close(idx, NoLock);
}
/* 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);
/*
* Drop identity property from all identity columns of partition.
*/
for (int attno = 0; attno < RelationGetNumberOfAttributes(partRel); attno++)
{
Form_pg_attribute attr = TupleDescAttr(partRel->rd_att, attno);
if (!attr->attisdropped && attr->attidentity)
ATExecDropIdentity(partRel, NameStr(attr->attname), false,
AccessExclusiveLock, true, true);
}
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);
}
/*
* Invalidate the parent's relcache so that the partition is no longer
* included in its partition descriptor.
*/
CacheInvalidateRelcache(rel);
/*
* 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)
{
List *children;
children = find_all_inheritors(RelationGetRelid(partRel),
AccessExclusiveLock, NULL);
foreach(cell, children)
{
CacheInvalidateRelcacheByRelid(lfirst_oid(cell));
}
}
}
/*
* ALTER TABLE ... DETACH PARTITION ... FINALIZE
*
* To use when a DETACH PARTITION command previously did not run to
* completion; this completes the detaching process.
*/
static ObjectAddress
ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
{
Relation partRel;
ObjectAddress address;
Snapshot snap = GetActiveSnapshot();
partRel = table_openrv(name, AccessExclusiveLock);
/*
* 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);
DetachPartitionFinalize(rel, partRel, true, InvalidOid);
ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
table_close(partRel, NoLock);
return address;
}
/*
* DetachAddConstraintIfNeeded
* Subroutine for ATExecDetachPartition. Create a constraint that
* takes the place of the partition constraint, but avoid creating
* a dupe if a constraint already exists which implies the needed
* constraint.
*/
static void
DetachAddConstraintIfNeeded(List **wqueue, Relation partRel)
{
List *constraintExpr;
constraintExpr = RelationGetPartitionQual(partRel);
constraintExpr = (List *) eval_const_expressions(NULL, (Node *) constraintExpr);
/*
* Avoid adding a new constraint if the needed constraint is implied by an
* existing constraint
*/
if (!PartConstraintImpliedByRelConstraint(partRel, constraintExpr))
{
AlteredTableInfo *tab;
Constraint *n;
tab = ATGetQueueEntry(wqueue, partRel);
/* Add constraint on partition, equivalent to the partition constraint */
n = makeNode(Constraint);
n->contype = CONSTR_CHECK;
n->conname = NULL;
n->location = -1;
n->is_no_inherit = false;
n->raw_expr = NULL;
n->cooked_expr = nodeToString(make_ands_explicit(constraintExpr));
n->initially_valid = true;
n->skip_validation = true;
/* It's a re-add, since it nominally already exists */
ATAddCheckNNConstraint(wqueue, tab, partRel, n,
true, false, true, ShareUpdateExclusiveLock);
}
}
/*
* 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;
objects = new_object_addresses();
/*
* 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;
/* Ignore triggers that weren't cloned */
if (!OidIsValid(pg_trigger->tgparentid))
continue;
/*
* Ignore internal triggers that are implementation objects of foreign
* keys, because these will be detached when the foreign keys
* themselves are.
*/
if (OidIsValid(pg_trigger->tgconstrrelid))
continue;
/*
* This is ugly, but necessary: remove the dependency markings on the
* trigger so that it can be removed.
*/
deleteDependencyRecordsForClass(TriggerRelationId, pg_trigger->oid,
TriggerRelationId,
DEPENDENCY_PARTITION_PRI);
deleteDependencyRecordsForClass(TriggerRelationId, pg_trigger->oid,
RelationRelationId,
DEPENDENCY_PARTITION_SEC);
/* remember this trigger to remove it below */
ObjectAddressSet(trig, TriggerRelationId, pg_trigger->oid);
add_exact_object_address(&trig, objects);
}
/* make the dependency removal visible to the deletion below */
CommandCounterIncrement();
performMultipleDeletions(objects, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
/* done */
free_object_addresses(objects);
systable_endscan(scan);
table_close(tgrel, RowExclusiveLock);
}
/*
* Before acquiring lock on an index, acquire the same lock on the owning
* table.
*/
struct AttachIndexCallbackState
{
Oid partitionOid;
Oid parentTblOid;
bool lockedParentTbl;
};
static void
RangeVarCallbackForAttachIndex(const RangeVar *rv, Oid relOid, Oid oldRelOid,
void *arg)
{
struct AttachIndexCallbackState *state;
Form_pg_class classform;
HeapTuple tuple;
state = (struct AttachIndexCallbackState *) arg;
if (!state->lockedParentTbl)
{
LockRelationOid(state->parentTblOid, AccessShareLock);
state->lockedParentTbl = true;
}
/*
* If we previously locked some other heap, and the name we're looking up
* no longer refers to an index on that relation, release the now-useless
* lock. XXX maybe we should do *after* we verify whether the index does
* not actually belong to the same relation ...
*/
if (relOid != oldRelOid && OidIsValid(state->partitionOid))
{
UnlockRelationOid(state->partitionOid, AccessShareLock);
state->partitionOid = InvalidOid;
}
/* 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);
}
/*
* ALTER INDEX i1 ATTACH PARTITION i2
*/
static ObjectAddress
ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, RangeVar *name)
{
Relation partIdx;
Relation partTbl;
Relation parentTbl;
ObjectAddress address;
Oid partIdxId;
Oid currParent;
struct AttachIndexCallbackState state;
/*
* We need to obtain lock on the index 'name' to modify it, but we also
* need to read its owning table's tuple descriptor -- so we need to lock
* both. To avoid deadlocks, obtain lock on the table before doing so on
* the index. Furthermore, we need to examine the parent table of the
* partition, so lock that one too.
*/
state.partitionOid = InvalidOid;
state.parentTblOid = parentIdx->rd_index->indrelid;
state.lockedParentTbl = false;
partIdxId =
RangeVarGetRelidExtended(name, AccessExclusiveLock, 0,
RangeVarCallbackForAttachIndex,
(void *) &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));
/* Silently do nothing if already in the right state */
currParent = partIdx->rd_rel->relispartition ?
get_partition_parent(partIdxId, false) : InvalidOid;
if (currParent != RelationGetRelid(parentIdx))
{
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);
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++)
{
if (partDesc->oids[i] == state.partitionOid)
{
found = true;
break;
}
}
if (!found)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot attach index \"%s\" as a partition of index \"%s\"",
RelationGetRelationName(partIdx),
RelationGetRelationName(parentIdx)),
errdetail("Index \"%s\" is not an index on any partition of table \"%s\".",
RelationGetRelationName(partIdx),
RelationGetRelationName(parentTbl))));
/* Ensure the indexes are compatible */
childInfo = BuildIndexInfo(partIdx);
parentInfo = BuildIndexInfo(parentIdx);
attmap = build_attrmap_by_name(RelationGetDescr(partTbl),
RelationGetDescr(parentTbl),
false);
if (!CompareIndexInfo(childInfo, parentInfo,
partIdx->rd_indcollation,
parentIdx->rd_indcollation,
partIdx->rd_opfamily,
parentIdx->rd_opfamily,
attmap))
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))
{
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))));
}
/*
* 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;
}
/*
* Verify whether the given partition already contains an index attached
* to the given partitioned index. If so, raise an error.
*/
static void
refuseDupeIndexAttach(Relation parentIdx, Relation partIdx, Relation partitionTbl)
{
Oid existingIdx;
existingIdx = index_get_partition(partitionTbl,
RelationGetRelid(parentIdx));
if (OidIsValid(existingIdx))
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot attach index \"%s\" as a partition of index \"%s\"",
RelationGetRelationName(partIdx),
RelationGetRelationName(parentIdx)),
errdetail("Another index is already attached for partition \"%s\".",
RelationGetRelationName(partitionTbl))));
}
/*
* 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.
*/
static void
validatePartitionedIndex(Relation partedIdx, Relation partedTbl)
{
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);
/*
* 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;
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);
indexForm->indisvalid = true;
updated = true;
CatalogTupleUpdate(idxRel, &indTup->t_self, indTup);
table_close(idxRel, RowExclusiveLock);
heap_freetuple(indTup);
}
/*
* 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;
/* make sure we see the validation we just did */
CommandCounterIncrement();
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);
validatePartitionedIndex(parentIdx, parentTbl);
relation_close(parentIdx, AccessExclusiveLock);
relation_close(parentTbl, AccessExclusiveLock);
}
}
/*
* 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.
*/
static void
verifyPartitionIndexNotNull(IndexInfo *iinfo, Relation partition)
{
for (int i = 0; i < iinfo->ii_NumIndexKeyAttrs; i++)
{
Form_pg_attribute att = TupleDescAttr(RelationGetDescr(partition),
iinfo->ii_IndexAttrNumbers[i] - 1);
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)));
}
}
/*
* 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;
/*
* If no indexes, or no columns are referenceable by FKs, we can avoid the
* scan.
*/
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)
{
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);
}
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)
{
Oid constrOid = lfirst_oid(cell);
HeapTuple tuple;
Form_pg_constraint constrForm;
Relation rel;
Trigger trig = {0};
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for constraint %u", constrOid);
constrForm = (Form_pg_constraint) GETSTRUCT(tuple);
Assert(OidIsValid(constrForm->conparentid));
Assert(constrForm->confrelid == RelationGetRelid(partition));
/* prevent data changes into the referencing table until commit */
rel = table_open(constrForm->conrelid, ShareLock);
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 */
RI_PartitionRemove_Check(&trig, rel, partition);
ReleaseSysCache(tuple);
table_close(rel, NoLock);
}
}
/*
* resolve column compression specification to compression method.
*/
static char
GetAttributeCompression(Oid atttypid, const char *compression)
{
char cmethod;
if (compression == NULL || strcmp(compression, "default") == 0)
return InvalidCompressionMethod;
/*
* To specify a nondefault method, the column data type must be toastable.
* Note this says nothing about whether the column's attstorage setting
* permits compression; we intentionally allow attstorage and
* attcompression to be independent. But with a non-toastable type,
* attstorage could not be set to a value that would permit compression.
*
* We don't actually need to enforce this, since nothing bad would happen
* if attcompression were non-default; it would never be consulted. But
* it seems more user-friendly to complain about a certainly-useless
* attempt to set the property.
*/
if (!TypeIsToastable(atttypid))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("column data type %s does not support compression",
format_type_be(atttypid))));
cmethod = CompressionNameToMethod(compression);
if (!CompressionMethodIsValid(cmethod))
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("invalid compression method \"%s\"", compression)));
return cmethod;
}
/*
* resolve column storage specification
*/
static char
GetAttributeStorage(Oid atttypid, const char *storagemode)
{
char cstorage = 0;
if (pg_strcasecmp(storagemode, "plain") == 0)
cstorage = TYPSTORAGE_PLAIN;
else if (pg_strcasecmp(storagemode, "external") == 0)
cstorage = TYPSTORAGE_EXTERNAL;
else if (pg_strcasecmp(storagemode, "extended") == 0)
cstorage = TYPSTORAGE_EXTENDED;
else if (pg_strcasecmp(storagemode, "main") == 0)
cstorage = TYPSTORAGE_MAIN;
else if (pg_strcasecmp(storagemode, "default") == 0)
cstorage = get_typstorage(atttypid);
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("invalid storage type \"%s\"",
storagemode)));
/*
* safety check: do not allow toasted storage modes unless column datatype
* is TOAST-aware.
*/
if (!(cstorage == TYPSTORAGE_PLAIN || TypeIsToastable(atttypid)))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("column data type %s can only have storage PLAIN",
format_type_be(atttypid))));
return cstorage;
}
/*
* Struct with context of new partition for inserting rows from split partition
*/
typedef struct SplitPartitionContext
{
ExprState *partqualstate; /* expression for checking slot for partition
* (NULL for DEFAULT partition) */
BulkInsertState bistate; /* state of bulk inserts for partition */
TupleTableSlot *dstslot; /* slot for inserting row into partition */
Relation partRel; /* relation for partition */
} SplitPartitionContext;
/*
* createSplitPartitionContext: create context for partition and fill it
*/
static SplitPartitionContext *
createSplitPartitionContext(Relation partRel)
{
SplitPartitionContext *pc;
pc = (SplitPartitionContext *) palloc0(sizeof(SplitPartitionContext));
pc->partRel = partRel;
/*
* Prepare a BulkInsertState for table_tuple_insert. The FSM is empty, so
* don't bother using it.
*/
pc->bistate = GetBulkInsertState();
/* Create tuple slot for new partition. */
pc->dstslot = MakeSingleTupleTableSlot(RelationGetDescr(pc->partRel),
table_slot_callbacks(pc->partRel));
ExecStoreAllNullTuple(pc->dstslot);
return pc;
}
/*
* deleteSplitPartitionContext: delete context for partition
*/
static void
deleteSplitPartitionContext(SplitPartitionContext *pc, int ti_options)
{
ExecDropSingleTupleTableSlot(pc->dstslot);
FreeBulkInsertState(pc->bistate);
table_finish_bulk_insert(pc->partRel, ti_options);
pfree(pc);
}
/*
* moveSplitTableRows: scan split partition (splitRel) of partitioned table
* (rel) and move rows into new partitions.
*
* New partitions description:
* partlist: list of pointers to SinglePartitionSpec structures.
* newPartRels: list of Relations.
* defaultPartOid: oid of DEFAULT partition, for table rel.
*/
static void
moveSplitTableRows(Relation rel, Relation splitRel, List *partlist, List *newPartRels, Oid defaultPartOid)
{
/* The FSM is empty, so don't bother using it. */
int ti_options = TABLE_INSERT_SKIP_FSM;
CommandId mycid;
EState *estate;
ListCell *listptr,
*listptr2;
TupleTableSlot *srcslot;
ExprContext *econtext;
TableScanDesc scan;
Snapshot snapshot;
MemoryContext oldCxt;
List *partContexts = NIL;
TupleConversionMap *tuple_map;
SplitPartitionContext *defaultPartCtx = NULL,
*pc;
bool isOldDefaultPart = false;
mycid = GetCurrentCommandId(true);
estate = CreateExecutorState();
forboth(listptr, partlist, listptr2, newPartRels)
{
SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
pc = createSplitPartitionContext((Relation) lfirst(listptr2));
if (sps->bound->is_default)
{
/* We should not create constraint for detached DEFAULT partition. */
defaultPartCtx = pc;
}
else
{
List *partConstraint;
/* Build expression execution states for partition check quals. */
partConstraint = get_qual_from_partbound(rel, sps->bound);
partConstraint =
(List *) eval_const_expressions(NULL,
(Node *) partConstraint);
/* Make boolean expression for ExecCheck(). */
partConstraint = list_make1(make_ands_explicit(partConstraint));
/*
* Map the vars in the constraint expression from rel's attnos to
* splitRel's.
*/
partConstraint = map_partition_varattnos(partConstraint,
1, splitRel, rel);
pc->partqualstate =
ExecPrepareExpr((Expr *) linitial(partConstraint), estate);
Assert(pc->partqualstate != NULL);
}
/* Store partition context into list. */
partContexts = lappend(partContexts, pc);
}
/*
* Create partition context for DEFAULT partition. We can insert values
* into this partition in case spaces with values between new partitions.
*/
if (!defaultPartCtx && OidIsValid(defaultPartOid))
{
/* Indicate that we allocate context for old DEFAULT partition */
isOldDefaultPart = true;
defaultPartCtx = createSplitPartitionContext(table_open(defaultPartOid, AccessExclusiveLock));
}
econtext = GetPerTupleExprContext(estate);
/* Create necessary tuple slot. */
srcslot = MakeSingleTupleTableSlot(RelationGetDescr(splitRel),
table_slot_callbacks(splitRel));
/*
* Map computing for moving attributes of split partition to new partition
* (for first new partition, but other new partitions can use the same
* map).
*/
pc = (SplitPartitionContext *) lfirst(list_head(partContexts));
tuple_map = convert_tuples_by_name(RelationGetDescr(splitRel),
RelationGetDescr(pc->partRel));
/* Scan through the rows. */
snapshot = RegisterSnapshot(GetLatestSnapshot());
scan = table_beginscan(splitRel, snapshot, 0, NULL);
/*
* Switch to per-tuple memory context and reset it for each tuple
* produced, so we don't leak memory.
*/
oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot))
{
bool found = false;
TupleTableSlot *insertslot;
/* Extract data from old tuple. */
slot_getallattrs(srcslot);
econtext->ecxt_scantuple = srcslot;
/* Search partition for current slot srcslot. */
foreach(listptr, partContexts)
{
pc = (SplitPartitionContext *) lfirst(listptr);
if (pc->partqualstate /* skip DEFAULT partition */ &&
ExecCheck(pc->partqualstate, econtext))
{
found = true;
break;
}
ResetExprContext(econtext);
}
if (!found)
{
/* Use DEFAULT partition if it exists. */
if (defaultPartCtx)
pc = defaultPartCtx;
else
ereport(ERROR,
(errcode(ERRCODE_CHECK_VIOLATION),
errmsg("can not find partition for split partition row"),
errtable(splitRel)));
}
if (tuple_map)
{
/* Need to use map to copy attributes. */
insertslot = execute_attr_map_slot(tuple_map->attrMap, srcslot, pc->dstslot);
}
else
{
/* Copy attributes directly. */
insertslot = pc->dstslot;
ExecClearTuple(insertslot);
memcpy(insertslot->tts_values, srcslot->tts_values,
sizeof(Datum) * srcslot->tts_nvalid);
memcpy(insertslot->tts_isnull, srcslot->tts_isnull,
sizeof(bool) * srcslot->tts_nvalid);
ExecStoreVirtualTuple(insertslot);
}
/* Write the tuple out to the new relation. */
table_tuple_insert(pc->partRel, insertslot, mycid,
ti_options, pc->bistate);
ResetExprContext(econtext);
CHECK_FOR_INTERRUPTS();
}
MemoryContextSwitchTo(oldCxt);
table_endscan(scan);
UnregisterSnapshot(snapshot);
if (tuple_map)
free_conversion_map(tuple_map);
ExecDropSingleTupleTableSlot(srcslot);
FreeExecutorState(estate);
foreach(listptr, partContexts)
deleteSplitPartitionContext((SplitPartitionContext *) lfirst(listptr), ti_options);
/* Need to close table and free buffers for DEFAULT partition. */
if (isOldDefaultPart)
{
Relation defaultPartRel = defaultPartCtx->partRel;
deleteSplitPartitionContext(defaultPartCtx, ti_options);
/* Keep the lock until commit. */
table_close(defaultPartRel, NoLock);
}
}
/*
* createPartitionTable: create table for a new partition with given name
* (newPartName) like table (modelRelName)
*
* Emulates command: CREATE TABLE <newPartName> (LIKE <modelRelName>
* INCLUDING ALL EXCLUDING INDEXES EXCLUDING IDENTITY)
*/
static void
createPartitionTable(RangeVar *newPartName, RangeVar *modelRelName,
AlterTableUtilityContext *context)
{
CreateStmt *createStmt;
TableLikeClause *tlc;
PlannedStmt *wrapper;
createStmt = makeNode(CreateStmt);
createStmt->relation = newPartName;
createStmt->tableElts = NIL;
createStmt->inhRelations = NIL;
createStmt->constraints = NIL;
createStmt->options = NIL;
createStmt->oncommit = ONCOMMIT_NOOP;
createStmt->tablespacename = NULL;
createStmt->if_not_exists = false;
tlc = makeNode(TableLikeClause);
tlc->relation = modelRelName;
/*
* Indexes will be inherited on "attach new partitions" stage, after data
* moving.
*/
tlc->options = CREATE_TABLE_LIKE_ALL & ~(CREATE_TABLE_LIKE_INDEXES | CREATE_TABLE_LIKE_IDENTITY);
tlc->relationOid = InvalidOid;
createStmt->tableElts = lappend(createStmt->tableElts, tlc);
/* Need to make a wrapper PlannedStmt. */
wrapper = makeNode(PlannedStmt);
wrapper->commandType = CMD_UTILITY;
wrapper->canSetTag = false;
wrapper->utilityStmt = (Node *) createStmt;
wrapper->stmt_location = context->pstmt->stmt_location;
wrapper->stmt_len = context->pstmt->stmt_len;
ProcessUtility(wrapper,
context->queryString,
false,
PROCESS_UTILITY_SUBCOMMAND,
NULL,
NULL,
None_Receiver,
NULL);
}
/*
* ALTER TABLE <name> SPLIT PARTITION <partition-name> INTO <partition-list>
*/
static void
ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
PartitionCmd *cmd, AlterTableUtilityContext *context)
{
Relation splitRel;
Oid splitRelOid;
char relname[NAMEDATALEN];
Oid namespaceId;
ListCell *listptr,
*listptr2;
bool isSameName = false;
char tmpRelName[NAMEDATALEN];
List *newPartRels = NIL;
ObjectAddress object;
RangeVar *parentName;
Oid defaultPartOid;
defaultPartOid = get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true));
/*
* We are going to detach and remove this partition: need to use exclusive
* lock for preventing DML-queries to the partition.
*/
splitRel = table_openrv(cmd->name, AccessExclusiveLock);
splitRelOid = RelationGetRelid(splitRel);
/* Check descriptions of new partitions. */
foreach(listptr, cmd->partlist)
{
Oid existing_relid;
SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
strlcpy(relname, sps->name->relname, NAMEDATALEN);
/*
* Look up the namespace in which we are supposed to create the
* partition, check we have permission to create there, lock it
* against concurrent drop, and mark stmt->relation as
* RELPERSISTENCE_TEMP if a temporary namespace is selected.
*/
namespaceId =
RangeVarGetAndCheckCreationNamespace(sps->name, NoLock, NULL);
/*
* This would fail later on anyway if the relation already exists. But
* by catching it here we can emit a nicer error message.
*/
existing_relid = get_relname_relid(relname, namespaceId);
if (existing_relid == splitRelOid && !isSameName)
/* One new partition can have the same name as split partition. */
isSameName = true;
else if (existing_relid != InvalidOid)
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_TABLE),
errmsg("relation \"%s\" already exists", relname)));
}
/* Detach split partition. */
RemoveInheritance(splitRel, rel, false);
/* Do the final part of detaching. */
DetachPartitionFinalize(rel, splitRel, false, defaultPartOid);
/*
* If new partition has the same name as split partition then we should
* rename split partition for reusing name.
*/
if (isSameName)
{
/*
* We must bump the command counter to make the split partition tuple
* visible for renaming.
*/
CommandCounterIncrement();
/* Rename partition. */
sprintf(tmpRelName, "split-%u-%X-tmp", RelationGetRelid(rel), MyProcPid);
RenameRelationInternal(splitRelOid, tmpRelName, false, false);
/*
* We must bump the command counter to make the split partition tuple
* visible after renaming.
*/
CommandCounterIncrement();
}
/* Create new partitions (like split partition), without indexes. */
parentName = makeRangeVar(get_namespace_name(RelationGetNamespace(rel)),
RelationGetRelationName(rel), -1);
foreach(listptr, cmd->partlist)
{
SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
Relation newPartRel;
createPartitionTable(sps->name, parentName, context);
/* Open the new partition and acquire exclusive lock on it. */
newPartRel = table_openrv(sps->name, AccessExclusiveLock);
newPartRels = lappend(newPartRels, newPartRel);
}
/* Copy data from split partition to new partitions. */
moveSplitTableRows(rel, splitRel, cmd->partlist, newPartRels, defaultPartOid);
/* Keep the lock until commit. */
table_close(splitRel, NoLock);
/* Attach new partitions to partitioned table. */
forboth(listptr, cmd->partlist, listptr2, newPartRels)
{
SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
Relation newPartRel = (Relation) lfirst(listptr2);
/*
* wqueue = NULL: verification for each cloned constraint is not
* needed.
*/
attachPartitionTable(NULL, rel, newPartRel, sps->bound);
/* Keep the lock until commit. */
table_close(newPartRel, NoLock);
}
/* Drop split partition. */
object.classId = RelationRelationId;
object.objectId = splitRelOid;
object.objectSubId = 0;
/* Probably DROP_CASCADE is not needed. */
performDeletion(&object, DROP_RESTRICT, 0);
}
/*
* moveMergedTablesRows: scan partitions to be merged (mergingPartitionsList)
* of the partitioned table (rel) and move rows into the new partition
* (newPartRel).
*/
static void
moveMergedTablesRows(Relation rel, List *mergingPartitionsList,
Relation newPartRel)
{
CommandId mycid;
/* The FSM is empty, so don't bother using it. */
int ti_options = TABLE_INSERT_SKIP_FSM;
ListCell *listptr;
BulkInsertState bistate; /* state of bulk inserts for partition */
TupleTableSlot *dstslot;
mycid = GetCurrentCommandId(true);
/* Prepare a BulkInsertState for table_tuple_insert. */
bistate = GetBulkInsertState();
/* Create necessary tuple slot. */
dstslot = MakeSingleTupleTableSlot(RelationGetDescr(newPartRel),
table_slot_callbacks(newPartRel));
ExecStoreAllNullTuple(dstslot);
foreach(listptr, mergingPartitionsList)
{
Relation mergingPartition = (Relation) lfirst(listptr);
TupleTableSlot *srcslot;
TupleConversionMap *tuple_map;
TableScanDesc scan;
Snapshot snapshot;
/* Create tuple slot for new partition. */
srcslot = MakeSingleTupleTableSlot(RelationGetDescr(mergingPartition),
table_slot_callbacks(mergingPartition));
/*
* Map computing for moving attributes of merged partition to new
* partition.
*/
tuple_map = convert_tuples_by_name(RelationGetDescr(mergingPartition),
RelationGetDescr(newPartRel));
/* Scan through the rows. */
snapshot = RegisterSnapshot(GetLatestSnapshot());
scan = table_beginscan(mergingPartition, snapshot, 0, NULL);
while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot))
{
TupleTableSlot *insertslot;
/* Extract data from old tuple. */
slot_getallattrs(srcslot);
if (tuple_map)
{
/* Need to use map to copy attributes. */
insertslot = execute_attr_map_slot(tuple_map->attrMap, srcslot, dstslot);
}
else
{
/* Copy attributes directly. */
insertslot = dstslot;
ExecClearTuple(insertslot);
memcpy(insertslot->tts_values, srcslot->tts_values,
sizeof(Datum) * srcslot->tts_nvalid);
memcpy(insertslot->tts_isnull, srcslot->tts_isnull,
sizeof(bool) * srcslot->tts_nvalid);
ExecStoreVirtualTuple(insertslot);
}
/* Write the tuple out to the new relation. */
table_tuple_insert(newPartRel, insertslot, mycid,
ti_options, bistate);
CHECK_FOR_INTERRUPTS();
}
table_endscan(scan);
UnregisterSnapshot(snapshot);
if (tuple_map)
free_conversion_map(tuple_map);
ExecDropSingleTupleTableSlot(srcslot);
}
ExecDropSingleTupleTableSlot(dstslot);
FreeBulkInsertState(bistate);
table_finish_bulk_insert(newPartRel, ti_options);
}
/*
* ALTER TABLE <name> MERGE PARTITIONS <partition-list> INTO <partition-name>
*/
static void
ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
PartitionCmd *cmd, AlterTableUtilityContext *context)
{
Relation newPartRel;
ListCell *listptr;
List *mergingPartitionsList = NIL;
Oid defaultPartOid;
/*
* Lock all merged partitions, check them and create list with partitions
* contexts.
*/
foreach(listptr, cmd->partlist)
{
RangeVar *name = (RangeVar *) lfirst(listptr);
Relation mergingPartition;
/*
* We are going to detach and remove this partition: need to use
* exclusive lock for preventing DML-queries to the partition.
*/
mergingPartition = table_openrv(name, AccessExclusiveLock);
/*
* Checking that two partitions have the same name was before, in
* function transformPartitionCmdForMerge().
*/
if (equal(name, cmd->name))
{
/* One new partition can have the same name as merged partition. */
char tmpRelName[NAMEDATALEN];
/* Generate temporary name. */
sprintf(tmpRelName, "merge-%u-%X-tmp", RelationGetRelid(rel), MyProcPid);
/*
* Rename the existing partition with a temporary name, leaving it
* free for the new partition. We don't need to care about this
* in the future because we're going to eventually drop the
* existing partition anyway.
*/
RenameRelationInternal(RelationGetRelid(mergingPartition),
tmpRelName, false, false);
/*
* We must bump the command counter to make the new partition
* tuple visible for rename.
*/
CommandCounterIncrement();
}
/* Store a next merging partition into the list. */
mergingPartitionsList = lappend(mergingPartitionsList,
mergingPartition);
}
/* Detach all merged partitions. */
defaultPartOid =
get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true));
foreach(listptr, mergingPartitionsList)
{
Relation mergingPartition = (Relation) lfirst(listptr);
/* Remove the pg_inherits row first. */
RemoveInheritance(mergingPartition, rel, false);
/* Do the final part of detaching. */
DetachPartitionFinalize(rel, mergingPartition, false, defaultPartOid);
}
createPartitionTable(cmd->name,
makeRangeVar(get_namespace_name(RelationGetNamespace(rel)),
RelationGetRelationName(rel), -1),
context);
/*
* Open the new partition and acquire exclusive lock on it. This will
* stop all the operations with partitioned table. This might seem
* excessive, but this is the way we make sure nobody is planning queries
* involving merging partitions.
*/
newPartRel = table_openrv(cmd->name, AccessExclusiveLock);
/* Copy data from merged partitions to new partition. */
moveMergedTablesRows(rel, mergingPartitionsList, newPartRel);
/* Drop the current partitions before attaching the new one. */
foreach(listptr, mergingPartitionsList)
{
ObjectAddress object;
Relation mergingPartition = (Relation) lfirst(listptr);
/* Get relation id before table_close() call. */
object.objectId = RelationGetRelid(mergingPartition);
object.classId = RelationRelationId;
object.objectSubId = 0;
/* Keep the lock until commit. */
table_close(mergingPartition, NoLock);
performDeletion(&object, DROP_RESTRICT, 0);
}
list_free(mergingPartitionsList);
/*
* Attach a new partition to the partitioned table. wqueue = NULL:
* verification for each cloned constraint is not needed.
*/
attachPartitionTable(NULL, rel, newPartRel, cmd->bound);
/* Keep the lock until commit. */
table_close(newPartRel, NoLock);
}