1
0
mirror of https://github.com/postgres/postgres.git synced 2025-07-03 20:02:46 +03:00

Event Trigger for table_rewrite

Generate a table_rewrite event when ALTER TABLE
attempts to rewrite a table. Provide helper
functions to identify table and reason.

Intended use case is to help assess or to react
to schema changes that might hold exclusive locks
for long periods.

Dimitri Fontaine, triggering an edit by Simon Riggs

Reviewed in detail by Michael Paquier
This commit is contained in:
Simon Riggs
2014-12-08 00:55:28 +09:00
parent b8e33a85d4
commit 618c9430a8
13 changed files with 556 additions and 39 deletions

View File

@ -42,11 +42,16 @@
#include "utils/syscache.h"
#include "tcop/utility.h"
typedef struct EventTriggerQueryState
{
/* sql_drop */
slist_head SQLDropList;
bool in_sql_drop;
/* table_rewrite */
Oid table_rewrite_oid; /* InvalidOid, or set for table_rewrite event */
int table_rewrite_reason; /* AT_REWRITE reason */
MemoryContext cxt;
struct EventTriggerQueryState *previous;
} EventTriggerQueryState;
@ -119,11 +124,14 @@ static void AlterEventTriggerOwner_internal(Relation rel,
HeapTuple tup,
Oid newOwnerId);
static event_trigger_command_tag_check_result check_ddl_tag(const char *tag);
static event_trigger_command_tag_check_result check_table_rewrite_ddl_tag(
const char *tag);
static void error_duplicate_filter_variable(const char *defname);
static Datum filter_list_to_array(List *filterlist);
static Oid insert_event_trigger_tuple(char *trigname, char *eventname,
Oid evtOwner, Oid funcoid, List *tags);
static void validate_ddl_tags(const char *filtervar, List *taglist);
static void validate_table_rewrite_tags(const char *filtervar, List *taglist);
static void EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata);
/*
@ -154,7 +162,8 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
/* Validate event name. */
if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
strcmp(stmt->eventname, "ddl_command_end") != 0 &&
strcmp(stmt->eventname, "sql_drop") != 0)
strcmp(stmt->eventname, "sql_drop") != 0 &&
strcmp(stmt->eventname, "table_rewrite") != 0)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("unrecognized event name \"%s\"",
@ -183,6 +192,9 @@ CreateEventTrigger(CreateEventTrigStmt *stmt)
strcmp(stmt->eventname, "sql_drop") == 0)
&& tags != NULL)
validate_ddl_tags("tag", tags);
else if (strcmp(stmt->eventname, "table_rewrite") == 0
&& tags != NULL)
validate_table_rewrite_tags("tag", tags);
/*
* Give user a nice error message if an event trigger of the same name
@ -280,6 +292,38 @@ check_ddl_tag(const char *tag)
return EVENT_TRIGGER_COMMAND_TAG_OK;
}
/*
* Validate DDL command tags for event table_rewrite.
*/
static void
validate_table_rewrite_tags(const char *filtervar, List *taglist)
{
ListCell *lc;
foreach(lc, taglist)
{
const char *tag = strVal(lfirst(lc));
event_trigger_command_tag_check_result result;
result = check_table_rewrite_ddl_tag(tag);
if (result == EVENT_TRIGGER_COMMAND_TAG_NOT_SUPPORTED)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
/* translator: %s represents an SQL statement name */
errmsg("event triggers are not supported for %s",
tag)));
}
}
static event_trigger_command_tag_check_result
check_table_rewrite_ddl_tag(const char *tag)
{
if (pg_strcasecmp(tag, "ALTER TABLE") == 0)
return EVENT_TRIGGER_COMMAND_TAG_OK;
return EVENT_TRIGGER_COMMAND_TAG_NOT_SUPPORTED;
}
/*
* Complain about a duplicate filter variable.
*/
@ -641,8 +685,18 @@ EventTriggerCommonSetup(Node *parsetree,
const char *dbgtag;
dbgtag = CreateCommandTag(parsetree);
if (check_ddl_tag(dbgtag) != EVENT_TRIGGER_COMMAND_TAG_OK)
elog(ERROR, "unexpected command tag \"%s\"", dbgtag);
if (event == EVT_DDLCommandStart ||
event == EVT_DDLCommandEnd ||
event == EVT_SQLDrop)
{
if (check_ddl_tag(dbgtag) != EVENT_TRIGGER_COMMAND_TAG_OK)
elog(ERROR, "unexpected command tag \"%s\"", dbgtag);
}
else if (event == EVT_TableRewrite)
{
if (check_table_rewrite_ddl_tag(dbgtag) != EVENT_TRIGGER_COMMAND_TAG_OK)
elog(ERROR, "unexpected command tag \"%s\"", dbgtag);
}
}
#endif
@ -838,6 +892,80 @@ EventTriggerSQLDrop(Node *parsetree)
list_free(runlist);
}
/*
* Fire table_rewrite triggers.
*/
void
EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason)
{
List *runlist;
EventTriggerData trigdata;
elog(DEBUG1, "EventTriggerTableRewrite(%u)", tableOid);
/*
* Event Triggers are completely disabled in standalone mode. There are
* (at least) two reasons for this:
*
* 1. A sufficiently broken event trigger might not only render the
* database unusable, but prevent disabling itself to fix the situation.
* In this scenario, restarting in standalone mode provides an escape
* hatch.
*
* 2. BuildEventTriggerCache relies on systable_beginscan_ordered, and
* therefore will malfunction if pg_event_trigger's indexes are damaged.
* To allow recovery from a damaged index, we need some operating mode
* wherein event triggers are disabled. (Or we could implement
* heapscan-and-sort logic for that case, but having disaster recovery
* scenarios depend on code that's otherwise untested isn't appetizing.)
*/
if (!IsUnderPostmaster)
return;
runlist = EventTriggerCommonSetup(parsetree,
EVT_TableRewrite,
"table_rewrite",
&trigdata);
if (runlist == NIL)
return;
/*
* Make sure pg_event_trigger_table_rewrite_oid only works when running
* these triggers. Use PG_TRY to ensure table_rewrite_oid is reset even
* when one trigger fails. (This is perhaps not necessary, as the
* currentState variable will be removed shortly by our caller, but it
* seems better to play safe.)
*/
currentEventTriggerState->table_rewrite_oid = tableOid;
currentEventTriggerState->table_rewrite_reason = reason;
/* Run the triggers. */
PG_TRY();
{
EventTriggerInvoke(runlist, &trigdata);
}
PG_CATCH();
{
currentEventTriggerState->table_rewrite_oid = InvalidOid;
currentEventTriggerState->table_rewrite_reason = 0;
PG_RE_THROW();
}
PG_END_TRY();
currentEventTriggerState->table_rewrite_oid = InvalidOid;
currentEventTriggerState->table_rewrite_reason = 0;
/* Cleanup. */
list_free(runlist);
/*
* Make sure anything the event triggers did will be visible to the main
* command.
*/
CommandCounterIncrement();
}
/*
* Invoke each event trigger in a list of event triggers.
*/
@ -871,6 +999,8 @@ EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata)
FunctionCallInfoData fcinfo;
PgStat_FunctionCallUsage fcusage;
elog(DEBUG1, "EventTriggerInvoke %u", fnoid);
/*
* We want each event trigger to be able to see the results of the
* previous event trigger's action. Caller is responsible for any
@ -1026,8 +1156,9 @@ EventTriggerBeginCompleteQuery(void)
MemoryContext cxt;
/*
* Currently, sql_drop events are the only reason to have event trigger
* state at all; so if there are none, don't install one.
* Currently, sql_drop and table_rewrite events are the only reason to
* have event trigger state at all; so if there are none, don't install
* one.
*/
if (!trackDroppedObjectsNeeded())
return false;
@ -1041,6 +1172,7 @@ EventTriggerBeginCompleteQuery(void)
state->cxt = cxt;
slist_init(&(state->SQLDropList));
state->in_sql_drop = false;
state->table_rewrite_oid = InvalidOid;
state->previous = currentEventTriggerState;
currentEventTriggerState = state;
@ -1080,8 +1212,9 @@ EventTriggerEndCompleteQuery(void)
bool
trackDroppedObjectsNeeded(void)
{
/* true if any sql_drop event trigger exists */
return list_length(EventCacheLookup(EVT_SQLDrop)) > 0;
/* true if any sql_drop or table_rewrite event trigger exists */
return list_length(EventCacheLookup(EVT_SQLDrop)) > 0 ||
list_length(EventCacheLookup(EVT_TableRewrite)) > 0;
}
/*
@ -1297,3 +1430,46 @@ pg_event_trigger_dropped_objects(PG_FUNCTION_ARGS)
return (Datum) 0;
}
/*
* pg_event_trigger_table_rewrite_oid
*
* Make the Oid of the table going to be rewritten available to the user
* function run by the Event Trigger.
*/
Datum
pg_event_trigger_table_rewrite_oid(PG_FUNCTION_ARGS)
{
/*
* Protect this function from being called out of context
*/
if (!currentEventTriggerState ||
currentEventTriggerState->table_rewrite_oid == InvalidOid)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("%s can only be called in a table_rewrite event trigger function",
"pg_event_trigger_table_rewrite_oid()")));
PG_RETURN_OID(currentEventTriggerState->table_rewrite_oid);
}
/*
* pg_event_trigger_table_rewrite_reason
*
* Make the rewrite reason available to the user.
*/
Datum
pg_event_trigger_table_rewrite_reason(PG_FUNCTION_ARGS)
{
/*
* Protect this function from being called out of context
*/
if (!currentEventTriggerState ||
currentEventTriggerState->table_rewrite_reason == 0)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("%s can only be called in a table_rewrite event trigger function",
"pg_event_trigger_table_rewrite_reason()")));
PG_RETURN_INT32(currentEventTriggerState->table_rewrite_reason);
}

View File

@ -46,6 +46,7 @@
#include "commands/cluster.h"
#include "commands/comment.h"
#include "commands/defrem.h"
#include "commands/event_trigger.h"
#include "commands/policy.h"
#include "commands/sequence.h"
#include "commands/tablecmds.h"
@ -153,7 +154,7 @@ typedef struct AlteredTableInfo
List *constraints; /* List of NewConstraint */
List *newvals; /* List of NewColumnValue */
bool new_notnull; /* T if we added new NOT NULL constraints */
bool rewrite; /* T if a rewrite is forced */
int rewrite; /* Reason for forced rewrite, if any */
Oid newTableSpace; /* new tablespace; 0 means no change */
bool chgPersistence; /* T if SET LOGGED/UNLOGGED is used */
char newrelpersistence; /* if above is true */
@ -303,13 +304,15 @@ static void validateForeignKeyConstraint(char *conname,
static void createForeignKeyTriggers(Relation rel, Oid refRelOid,
Constraint *fkconstraint,
Oid constraintOid, Oid indexOid);
static void ATController(Relation rel, List *cmds, bool recurse, LOCKMODE lockmode);
static void ATController(AlterTableStmt *parsetree,
Relation rel, List *cmds, bool recurse, LOCKMODE lockmode);
static void ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
bool recurse, bool recursing, LOCKMODE lockmode);
static void ATRewriteCatalogs(List **wqueue, LOCKMODE lockmode);
static void ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
AlterTableCmd *cmd, LOCKMODE lockmode);
static void ATRewriteTables(List **wqueue, LOCKMODE lockmode);
static void ATRewriteTables(AlterTableStmt *parsetree,
List **wqueue, LOCKMODE lockmode);
static void ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode);
static AlteredTableInfo *ATGetQueueEntry(List **wqueue, Relation rel);
static void ATSimplePermissions(Relation rel, int allowed_targets);
@ -2724,7 +2727,8 @@ AlterTable(Oid relid, LOCKMODE lockmode, AlterTableStmt *stmt)
CheckTableNotInUse(rel, "ALTER TABLE");
ATController(rel, stmt->cmds, interpretInhOption(stmt->relation->inhOpt),
ATController(stmt,
rel, stmt->cmds, interpretInhOption(stmt->relation->inhOpt),
lockmode);
}
@ -2747,7 +2751,7 @@ AlterTableInternal(Oid relid, List *cmds, bool recurse)
rel = relation_open(relid, lockmode);
ATController(rel, cmds, recurse, lockmode);
ATController(NULL, rel, cmds, recurse, lockmode);
}
/*
@ -3015,8 +3019,15 @@ AlterTableGetLockLevel(List *cmds)
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(Relation rel, List *cmds, bool recurse, LOCKMODE lockmode)
ATController(AlterTableStmt *parsetree,
Relation rel, List *cmds, bool recurse, LOCKMODE lockmode)
{
List *wqueue = NIL;
ListCell *lcmd;
@ -3036,7 +3047,7 @@ ATController(Relation rel, List *cmds, bool recurse, LOCKMODE lockmode)
ATRewriteCatalogs(&wqueue, lockmode);
/* Phase 3: scan/rewrite tables as needed */
ATRewriteTables(&wqueue, lockmode);
ATRewriteTables(parsetree, &wqueue, lockmode);
}
/*
@ -3195,7 +3206,7 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
/* force rewrite if necessary; see comment in ATRewriteTables */
if (tab->chgPersistence)
{
tab->rewrite = true;
tab->rewrite |= AT_REWRITE_ALTER_PERSISTENCE;
tab->newrelpersistence = RELPERSISTENCE_PERMANENT;
}
pass = AT_PASS_MISC;
@ -3206,7 +3217,7 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
/* force rewrite if necessary; see comment in ATRewriteTables */
if (tab->chgPersistence)
{
tab->rewrite = true;
tab->rewrite |= AT_REWRITE_ALTER_PERSISTENCE;
tab->newrelpersistence = RELPERSISTENCE_UNLOGGED;
}
pass = AT_PASS_MISC;
@ -3607,7 +3618,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
* ATRewriteTables: ALTER TABLE phase 3
*/
static void
ATRewriteTables(List **wqueue, LOCKMODE lockmode)
ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode)
{
ListCell *ltab;
@ -3634,7 +3645,7 @@ ATRewriteTables(List **wqueue, LOCKMODE lockmode)
* constraints, so it's not necessary/appropriate to enforce them just
* during ALTER.)
*/
if (tab->newvals != NIL || tab->rewrite)
if (tab->newvals != NIL || tab->rewrite > 0)
{
Relation rel;
@ -3655,7 +3666,7 @@ ATRewriteTables(List **wqueue, LOCKMODE lockmode)
* and assigns a new relfilenode, we automatically create or drop an
* init fork for the relation as appropriate.
*/
if (tab->rewrite)
if (tab->rewrite > 0)
{
/* Build a temporary relation and copy data */
Relation OldHeap;
@ -3709,6 +3720,21 @@ ATRewriteTables(List **wqueue, LOCKMODE lockmode)
heap_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.
*
@ -4002,7 +4028,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode)
while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
if (tab->rewrite)
if (tab->rewrite > 0)
{
Oid tupOid = InvalidOid;
@ -4828,7 +4854,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
newval->expr = expression_planner(defval);
tab->newvals = lappend(tab->newvals, newval);
tab->rewrite = true;
tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
}
/*
@ -4845,7 +4871,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
* table to fix that.
*/
if (isOid)
tab->rewrite = true;
tab->rewrite |= AT_REWRITE_ALTER_OID;
/*
* Add needed dependency entries for the new column.
@ -5657,7 +5683,7 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
tab = ATGetQueueEntry(wqueue, rel);
/* Tell Phase 3 to physically remove the OID column */
tab->rewrite = true;
tab->rewrite |= AT_REWRITE_ALTER_OID;
}
}
@ -5683,7 +5709,7 @@ ATExecAddIndex(AlteredTableInfo *tab, Relation rel,
/* 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 || OidIsValid(stmt->oldNode);
skip_build = tab->rewrite > 0 || OidIsValid(stmt->oldNode);
/* suppress notices when rebuilding existing index */
quiet = is_rebuild;
@ -7686,7 +7712,7 @@ ATPrepAlterColumnType(List **wqueue,
tab->newvals = lappend(tab->newvals, newval);
if (ATColumnChangeRequiresRewrite(transform, attnum))
tab->rewrite = true;
tab->rewrite |= AT_REWRITE_COLUMN_REWRITE;
}
else if (transform)
ereport(ERROR,
@ -8431,7 +8457,7 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd,
con->old_pktable_oid = refRelId;
/* rewriting neither side of a FK */
if (con->contype == CONSTR_FOREIGN &&
!rewrite && !tab->rewrite)
!rewrite && tab->rewrite == 0)
TryReuseForeignKey(oldId, con);
cmd->subtype = AT_ReAddConstraint;
tab->subcmds[AT_PASS_OLD_CONSTR] =