mirror of
https://github.com/postgres/postgres.git
synced 2025-07-17 06:41:09 +03:00
Add an executor aminsert() hint mechanism that informs index AMs that the incoming index tuple (the tuple that accompanies the hint) is not being inserted by execution of an SQL statement that logically modifies any of the index's key columns. The hint is received by indexes when an UPDATE takes place that does not apply an optimization like heapam's HOT (though only for indexes where all key columns are logically unchanged). Any index tuple that receives the hint on insert is expected to be a duplicate of at least one existing older version that is needed for the same logical row. Related versions will typically be stored on the same index page, at least within index AMs that apply the hint. Recognizing the difference between MVCC version churn duplicates and true logical row duplicates at the index AM level can help with cleanup of garbage index tuples. Cleanup can intelligently target tuples that are likely to be garbage, without wasting too many cycles on less promising tuples/pages (index pages with little or no version churn). This is infrastructure for an upcoming commit that will teach nbtree to perform bottom-up index deletion. No index AM actually applies the hint just yet. Author: Peter Geoghegan <pg@bowt.ie> Reviewed-By: Victor Yegorov <vyegorov@gmail.com> Discussion: https://postgr.es/m/CAH2-Wz=CEKFa74EScx_hFVshCOn6AA5T-ajFASTdzipdkLTNQQ@mail.gmail.com
206 lines
6.5 KiB
C
206 lines
6.5 KiB
C
/*-------------------------------------------------------------------------
|
|
*
|
|
* constraint.c
|
|
* PostgreSQL CONSTRAINT support code.
|
|
*
|
|
* Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
|
|
* Portions Copyright (c) 1994, Regents of the University of California
|
|
*
|
|
* IDENTIFICATION
|
|
* src/backend/commands/constraint.c
|
|
*
|
|
*-------------------------------------------------------------------------
|
|
*/
|
|
#include "postgres.h"
|
|
|
|
#include "access/genam.h"
|
|
#include "access/heapam.h"
|
|
#include "access/tableam.h"
|
|
#include "catalog/index.h"
|
|
#include "commands/trigger.h"
|
|
#include "executor/executor.h"
|
|
#include "utils/builtins.h"
|
|
#include "utils/rel.h"
|
|
#include "utils/snapmgr.h"
|
|
|
|
|
|
/*
|
|
* unique_key_recheck - trigger function to do a deferred uniqueness check.
|
|
*
|
|
* This now also does deferred exclusion-constraint checks, so the name is
|
|
* somewhat historical.
|
|
*
|
|
* This is invoked as an AFTER ROW trigger for both INSERT and UPDATE,
|
|
* for any rows recorded as potentially violating a deferrable unique
|
|
* or exclusion 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";
|
|
ItemPointerData checktid;
|
|
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))
|
|
checktid = trigdata->tg_trigslot->tts_tid;
|
|
else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
|
|
checktid = trigdata->tg_newslot->tts_tid;
|
|
else
|
|
{
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
|
|
errmsg("function \"%s\" must be fired for INSERT or UPDATE",
|
|
funcname)));
|
|
ItemPointerSetInvalid(&checktid); /* keep compiler quiet */
|
|
}
|
|
|
|
slot = table_slot_create(trigdata->tg_relation, NULL);
|
|
|
|
/*
|
|
* If the row pointed at by checktid 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 e.g. 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 check ---
|
|
* effectively, we're applying the check against the live child row,
|
|
* although we can use the values from this row since by definition all
|
|
* columns of interest to us are the same.
|
|
*
|
|
* 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 = checktid;
|
|
{
|
|
IndexFetchTableData *scan = table_index_fetch_begin(trigdata->tg_relation);
|
|
bool call_again = false;
|
|
|
|
if (!table_index_fetch_tuple(scan, &tmptid, SnapshotSelf, slot,
|
|
&call_again, NULL))
|
|
{
|
|
/*
|
|
* All rows referenced by the index entry are dead, so skip the
|
|
* check.
|
|
*/
|
|
ExecDropSingleTupleTableSlot(slot);
|
|
table_index_fetch_end(scan);
|
|
return PointerGetDatum(NULL);
|
|
}
|
|
table_index_fetch_end(scan);
|
|
}
|
|
|
|
/*
|
|
* 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);
|
|
|
|
/*
|
|
* Typically the index won't have expressions, but if it does we need an
|
|
* EState to evaluate them. We need it for exclusion constraints too,
|
|
* even if they are just on simple columns.
|
|
*/
|
|
if (indexInfo->ii_Expressions != NIL ||
|
|
indexInfo->ii_ExclusionOps != NULL)
|
|
{
|
|
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. For exclusion
|
|
* constraints we check this in check_exclusion_constraint().
|
|
*/
|
|
FormIndexDatum(indexInfo, slot, estate, values, isnull);
|
|
|
|
/*
|
|
* Now do the appropriate check.
|
|
*/
|
|
if (indexInfo->ii_ExclusionOps == NULL)
|
|
{
|
|
/*
|
|
* Note: this is not a real insert; it is a check that the index entry
|
|
* that has already been inserted is unique. Passing the tuple's tid
|
|
* (i.e. unmodified by table_index_fetch_tuple()) is correct even if
|
|
* the row is now dead, because that is the TID the index will know
|
|
* about.
|
|
*/
|
|
index_insert(indexRel, values, isnull, &checktid,
|
|
trigdata->tg_relation, UNIQUE_CHECK_EXISTING,
|
|
false, indexInfo);
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* For exclusion constraints we just do the normal check, but now it's
|
|
* okay to throw error. In the HOT-update case, we must use the live
|
|
* HOT child's TID here, else check_exclusion_constraint will think
|
|
* the child is a conflict.
|
|
*/
|
|
check_exclusion_constraint(trigdata->tg_relation, indexRel, indexInfo,
|
|
&tmptid, values, isnull,
|
|
estate, false);
|
|
}
|
|
|
|
/*
|
|
* If that worked, then this index entry is unique or non-excluded, and we
|
|
* are done.
|
|
*/
|
|
if (estate != NULL)
|
|
FreeExecutorState(estate);
|
|
|
|
ExecDropSingleTupleTableSlot(slot);
|
|
|
|
index_close(indexRel, RowExclusiveLock);
|
|
|
|
return PointerGetDatum(NULL);
|
|
}
|