mirror of
https://github.com/postgres/postgres.git
synced 2025-07-03 20:02:46 +03:00
Support deferrable uniqueness constraints.
The current implementation fires an AFTER ROW trigger for each tuple that looks like it might be non-unique according to the index contents at the time of insertion. This works well as long as there aren't many conflicts, but won't scale to massive unique-key reassignments. Improving that case is a TODO item. Dean Rasheed
This commit is contained in:
166
src/backend/commands/constraint.c
Normal file
166
src/backend/commands/constraint.c
Normal file
@ -0,0 +1,166 @@
|
||||
/*-------------------------------------------------------------------------
|
||||
*
|
||||
* constraint.c
|
||||
* PostgreSQL CONSTRAINT support code.
|
||||
*
|
||||
* Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group
|
||||
* Portions Copyright (c) 1994, Regents of the University of California
|
||||
*
|
||||
* IDENTIFICATION
|
||||
* $PostgreSQL: pgsql/src/backend/commands/constraint.c,v 1.1 2009/07/29 20:56:18 tgl Exp $
|
||||
*
|
||||
*-------------------------------------------------------------------------
|
||||
*/
|
||||
#include "postgres.h"
|
||||
|
||||
#include "catalog/index.h"
|
||||
#include "commands/trigger.h"
|
||||
#include "executor/executor.h"
|
||||
#include "utils/builtins.h"
|
||||
#include "utils/tqual.h"
|
||||
|
||||
|
||||
/*
|
||||
* unique_key_recheck - trigger function to do a deferred uniqueness check.
|
||||
*
|
||||
* This is invoked as an AFTER ROW trigger for both INSERT and UPDATE,
|
||||
* for any rows recorded as potentially violating a deferrable unique
|
||||
* constraint.
|
||||
*
|
||||
* This may be an end-of-statement check, a commit-time check, or a
|
||||
* check triggered by a SET CONSTRAINTS command.
|
||||
*/
|
||||
Datum
|
||||
unique_key_recheck(PG_FUNCTION_ARGS)
|
||||
{
|
||||
TriggerData *trigdata = (TriggerData *) fcinfo->context;
|
||||
const char *funcname = "unique_key_recheck";
|
||||
HeapTuple new_row;
|
||||
ItemPointerData tmptid;
|
||||
Relation indexRel;
|
||||
IndexInfo *indexInfo;
|
||||
EState *estate;
|
||||
ExprContext *econtext;
|
||||
TupleTableSlot *slot;
|
||||
Datum values[INDEX_MAX_KEYS];
|
||||
bool isnull[INDEX_MAX_KEYS];
|
||||
|
||||
/*
|
||||
* Make sure this is being called as an AFTER ROW trigger. Note:
|
||||
* translatable error strings are shared with ri_triggers.c, so
|
||||
* resist the temptation to fold the function name into them.
|
||||
*/
|
||||
if (!CALLED_AS_TRIGGER(fcinfo))
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
|
||||
errmsg("function \"%s\" was not called by trigger manager",
|
||||
funcname)));
|
||||
|
||||
if (!TRIGGER_FIRED_AFTER(trigdata->tg_event) ||
|
||||
!TRIGGER_FIRED_FOR_ROW(trigdata->tg_event))
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
|
||||
errmsg("function \"%s\" must be fired AFTER ROW",
|
||||
funcname)));
|
||||
|
||||
/*
|
||||
* Get the new data that was inserted/updated.
|
||||
*/
|
||||
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
|
||||
new_row = trigdata->tg_trigtuple;
|
||||
else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
|
||||
new_row = trigdata->tg_newtuple;
|
||||
else
|
||||
{
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
|
||||
errmsg("function \"%s\" must be fired for INSERT or UPDATE",
|
||||
funcname)));
|
||||
new_row = NULL; /* keep compiler quiet */
|
||||
}
|
||||
|
||||
/*
|
||||
* If the new_row is now dead (ie, inserted and then deleted within our
|
||||
* transaction), we can skip the check. However, we have to be careful,
|
||||
* because this trigger gets queued only in response to index insertions;
|
||||
* which means it does not get queued for HOT updates. The row we are
|
||||
* called for might now be dead, but have a live HOT child, in which case
|
||||
* we still need to make the uniqueness check. Therefore we have to use
|
||||
* heap_hot_search, not just HeapTupleSatisfiesVisibility as is done in
|
||||
* the comparable test in RI_FKey_check.
|
||||
*
|
||||
* This might look like just an optimization, because the index AM will
|
||||
* make this identical test before throwing an error. But it's actually
|
||||
* needed for correctness, because the index AM will also throw an error
|
||||
* if it doesn't find the index entry for the row. If the row's dead then
|
||||
* it's possible the index entry has also been marked dead, and even
|
||||
* removed.
|
||||
*/
|
||||
tmptid = new_row->t_self;
|
||||
if (!heap_hot_search(&tmptid, trigdata->tg_relation, SnapshotSelf, NULL))
|
||||
{
|
||||
/*
|
||||
* All rows in the HOT chain are dead, so skip the check.
|
||||
*/
|
||||
return PointerGetDatum(NULL);
|
||||
}
|
||||
|
||||
/*
|
||||
* Open the index, acquiring a RowExclusiveLock, just as if we were
|
||||
* going to update it. (This protects against possible changes of the
|
||||
* index schema, not against concurrent updates.)
|
||||
*/
|
||||
indexRel = index_open(trigdata->tg_trigger->tgconstrindid,
|
||||
RowExclusiveLock);
|
||||
indexInfo = BuildIndexInfo(indexRel);
|
||||
|
||||
/*
|
||||
* The heap tuple must be put into a slot for FormIndexDatum.
|
||||
*/
|
||||
slot = MakeSingleTupleTableSlot(RelationGetDescr(trigdata->tg_relation));
|
||||
|
||||
ExecStoreTuple(new_row, slot, InvalidBuffer, false);
|
||||
|
||||
/*
|
||||
* Typically the index won't have expressions, but if it does we need
|
||||
* an EState to evaluate them.
|
||||
*/
|
||||
if (indexInfo->ii_Expressions != NIL)
|
||||
{
|
||||
estate = CreateExecutorState();
|
||||
econtext = GetPerTupleExprContext(estate);
|
||||
econtext->ecxt_scantuple = slot;
|
||||
}
|
||||
else
|
||||
estate = NULL;
|
||||
|
||||
/*
|
||||
* Form the index values and isnull flags for the index entry that
|
||||
* we need to check.
|
||||
*
|
||||
* Note: if the index uses functions that are not as immutable as they
|
||||
* are supposed to be, this could produce an index tuple different from
|
||||
* the original. The index AM can catch such errors by verifying that
|
||||
* it finds a matching index entry with the tuple's TID.
|
||||
*/
|
||||
FormIndexDatum(indexInfo, slot, estate, values, isnull);
|
||||
|
||||
/*
|
||||
* Now do the uniqueness check. This is not a real insert; it is a
|
||||
* check that the index entry that has already been inserted is unique.
|
||||
*/
|
||||
index_insert(indexRel, values, isnull, &(new_row->t_self),
|
||||
trigdata->tg_relation, UNIQUE_CHECK_EXISTING);
|
||||
|
||||
/*
|
||||
* If that worked, then this index entry is unique, and we are done.
|
||||
*/
|
||||
if (estate != NULL)
|
||||
FreeExecutorState(estate);
|
||||
|
||||
ExecDropSingleTupleTableSlot(slot);
|
||||
|
||||
index_close(indexRel, RowExclusiveLock);
|
||||
|
||||
return PointerGetDatum(NULL);
|
||||
}
|
Reference in New Issue
Block a user