LCOV - code coverage report
Current view: top level - src/backend/commands - constraint.c (source / functions) Coverage Total Hit
Test: Code coverage Lines: 87.9 % 58 51
Test Date: 2026-01-26 10:56:24 Functions: 100.0 % 1 1
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 44.1 % 34 15

             Branch data     Line data    Source code
       1                 :             : /*-------------------------------------------------------------------------
       2                 :             :  *
       3                 :             :  * constraint.c
       4                 :             :  *        PostgreSQL CONSTRAINT support code.
       5                 :             :  *
       6                 :             :  * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
       7                 :             :  * Portions Copyright (c) 1994, Regents of the University of California
       8                 :             :  *
       9                 :             :  * IDENTIFICATION
      10                 :             :  *        src/backend/commands/constraint.c
      11                 :             :  *
      12                 :             :  *-------------------------------------------------------------------------
      13                 :             :  */
      14                 :             : #include "postgres.h"
      15                 :             : 
      16                 :             : #include "access/genam.h"
      17                 :             : #include "access/tableam.h"
      18                 :             : #include "catalog/index.h"
      19                 :             : #include "commands/trigger.h"
      20                 :             : #include "executor/executor.h"
      21                 :             : #include "utils/fmgrprotos.h"
      22                 :             : #include "utils/snapmgr.h"
      23                 :             : 
      24                 :             : 
      25                 :             : /*
      26                 :             :  * unique_key_recheck - trigger function to do a deferred uniqueness check.
      27                 :             :  *
      28                 :             :  * This now also does deferred exclusion-constraint checks, so the name is
      29                 :             :  * somewhat historical.
      30                 :             :  *
      31                 :             :  * This is invoked as an AFTER ROW trigger for both INSERT and UPDATE,
      32                 :             :  * for any rows recorded as potentially violating a deferrable unique
      33                 :             :  * or exclusion constraint.
      34                 :             :  *
      35                 :             :  * This may be an end-of-statement check, a commit-time check, or a
      36                 :             :  * check triggered by a SET CONSTRAINTS command.
      37                 :             :  */
      38                 :             : Datum
      39                 :          20 : unique_key_recheck(PG_FUNCTION_ARGS)
      40                 :             : {
      41                 :          20 :         TriggerData *trigdata = (TriggerData *) fcinfo->context;
      42                 :          20 :         const char *funcname = "unique_key_recheck";
      43                 :          20 :         ItemPointerData checktid;
      44                 :          20 :         ItemPointerData tmptid;
      45                 :          20 :         Relation        indexRel;
      46                 :          20 :         IndexInfo  *indexInfo;
      47                 :          20 :         EState     *estate;
      48                 :          20 :         ExprContext *econtext;
      49                 :          20 :         TupleTableSlot *slot;
      50                 :          20 :         Datum           values[INDEX_MAX_KEYS];
      51                 :          20 :         bool            isnull[INDEX_MAX_KEYS];
      52                 :             : 
      53                 :             :         /*
      54                 :             :          * Make sure this is being called as an AFTER ROW trigger.  Note:
      55                 :             :          * translatable error strings are shared with ri_triggers.c, so resist the
      56                 :             :          * temptation to fold the function name into them.
      57                 :             :          */
      58         [ +  - ]:          20 :         if (!CALLED_AS_TRIGGER(fcinfo))
      59   [ #  #  #  # ]:           0 :                 ereport(ERROR,
      60                 :             :                                 (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
      61                 :             :                                  errmsg("function \"%s\" was not called by trigger manager",
      62                 :             :                                                 funcname)));
      63                 :             : 
      64         [ +  - ]:          20 :         if (!TRIGGER_FIRED_AFTER(trigdata->tg_event) ||
      65                 :          20 :                 !TRIGGER_FIRED_FOR_ROW(trigdata->tg_event))
      66   [ #  #  #  # ]:           0 :                 ereport(ERROR,
      67                 :             :                                 (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
      68                 :             :                                  errmsg("function \"%s\" must be fired AFTER ROW",
      69                 :             :                                                 funcname)));
      70                 :             : 
      71                 :             :         /*
      72                 :             :          * Get the new data that was inserted/updated.
      73                 :             :          */
      74         [ +  + ]:          20 :         if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
      75                 :          14 :                 checktid = trigdata->tg_trigslot->tts_tid;
      76         [ +  - ]:           6 :         else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
      77                 :           6 :                 checktid = trigdata->tg_newslot->tts_tid;
      78                 :             :         else
      79                 :             :         {
      80   [ #  #  #  # ]:           0 :                 ereport(ERROR,
      81                 :             :                                 (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
      82                 :             :                                  errmsg("function \"%s\" must be fired for INSERT or UPDATE",
      83                 :             :                                                 funcname)));
      84                 :           0 :                 ItemPointerSetInvalid(&checktid);   /* keep compiler quiet */
      85                 :             :         }
      86                 :             : 
      87                 :          20 :         slot = table_slot_create(trigdata->tg_relation, NULL);
      88                 :             : 
      89                 :             :         /*
      90                 :             :          * If the row pointed at by checktid is now dead (ie, inserted and then
      91                 :             :          * deleted within our transaction), we can skip the check.  However, we
      92                 :             :          * have to be careful, because this trigger gets queued only in response
      93                 :             :          * to index insertions; which means it does not get queued e.g. for HOT
      94                 :             :          * updates.  The row we are called for might now be dead, but have a live
      95                 :             :          * HOT child, in which case we still need to make the check ---
      96                 :             :          * effectively, we're applying the check against the live child row,
      97                 :             :          * although we can use the values from this row since by definition all
      98                 :             :          * columns of interest to us are the same.
      99                 :             :          *
     100                 :             :          * This might look like just an optimization, because the index AM will
     101                 :             :          * make this identical test before throwing an error.  But it's actually
     102                 :             :          * needed for correctness, because the index AM will also throw an error
     103                 :             :          * if it doesn't find the index entry for the row.  If the row's dead then
     104                 :             :          * it's possible the index entry has also been marked dead, and even
     105                 :             :          * removed.
     106                 :             :          */
     107                 :          20 :         tmptid = checktid;
     108                 :             :         {
     109                 :          20 :                 IndexFetchTableData *scan = table_index_fetch_begin(trigdata->tg_relation);
     110                 :          20 :                 bool            call_again = false;
     111                 :             : 
     112         [ +  - ]:          20 :                 if (!table_index_fetch_tuple(scan, &tmptid, SnapshotSelf, slot,
     113                 :             :                                                                          &call_again, NULL))
     114                 :             :                 {
     115                 :             :                         /*
     116                 :             :                          * All rows referenced by the index entry are dead, so skip the
     117                 :             :                          * check.
     118                 :             :                          */
     119                 :           0 :                         ExecDropSingleTupleTableSlot(slot);
     120                 :           0 :                         table_index_fetch_end(scan);
     121                 :           0 :                         return PointerGetDatum(NULL);
     122                 :             :                 }
     123                 :          20 :                 table_index_fetch_end(scan);
     124         [ -  + ]:          20 :         }
     125                 :             : 
     126                 :             :         /*
     127                 :             :          * Open the index, acquiring a RowExclusiveLock, just as if we were going
     128                 :             :          * to update it.  (This protects against possible changes of the index
     129                 :             :          * schema, not against concurrent updates.)
     130                 :             :          */
     131                 :          20 :         indexRel = index_open(trigdata->tg_trigger->tgconstrindid,
     132                 :             :                                                   RowExclusiveLock);
     133                 :          20 :         indexInfo = BuildIndexInfo(indexRel);
     134                 :             : 
     135                 :             :         /*
     136                 :             :          * Typically the index won't have expressions, but if it does we need an
     137                 :             :          * EState to evaluate them.  We need it for exclusion constraints too,
     138                 :             :          * even if they are just on simple columns.
     139                 :             :          */
     140   [ +  -  +  + ]:          20 :         if (indexInfo->ii_Expressions != NIL ||
     141                 :          20 :                 indexInfo->ii_ExclusionOps != NULL)
     142                 :             :         {
     143                 :           4 :                 estate = CreateExecutorState();
     144         [ -  + ]:           4 :                 econtext = GetPerTupleExprContext(estate);
     145                 :           4 :                 econtext->ecxt_scantuple = slot;
     146                 :           4 :         }
     147                 :             :         else
     148                 :          16 :                 estate = NULL;
     149                 :             : 
     150                 :             :         /*
     151                 :             :          * Form the index values and isnull flags for the index entry that we need
     152                 :             :          * to check.
     153                 :             :          *
     154                 :             :          * Note: if the index uses functions that are not as immutable as they are
     155                 :             :          * supposed to be, this could produce an index tuple different from the
     156                 :             :          * original.  The index AM can catch such errors by verifying that it
     157                 :             :          * finds a matching index entry with the tuple's TID.  For exclusion
     158                 :             :          * constraints we check this in check_exclusion_constraint().
     159                 :             :          */
     160                 :          20 :         FormIndexDatum(indexInfo, slot, estate, values, isnull);
     161                 :             : 
     162                 :             :         /*
     163                 :             :          * Now do the appropriate check.
     164                 :             :          */
     165         [ +  + ]:          20 :         if (indexInfo->ii_ExclusionOps == NULL)
     166                 :             :         {
     167                 :             :                 /*
     168                 :             :                  * Note: this is not a real insert; it is a check that the index entry
     169                 :             :                  * that has already been inserted is unique.  Passing the tuple's tid
     170                 :             :                  * (i.e. unmodified by table_index_fetch_tuple()) is correct even if
     171                 :             :                  * the row is now dead, because that is the TID the index will know
     172                 :             :                  * about.
     173                 :             :                  */
     174                 :          32 :                 index_insert(indexRel, values, isnull, &checktid,
     175                 :          16 :                                          trigdata->tg_relation, UNIQUE_CHECK_EXISTING,
     176                 :          16 :                                          false, indexInfo);
     177                 :             : 
     178                 :             :                 /* Cleanup cache possibly initialized by index_insert. */
     179                 :          16 :                 index_insert_cleanup(indexRel, indexInfo);
     180                 :          16 :         }
     181                 :             :         else
     182                 :             :         {
     183                 :             :                 /*
     184                 :             :                  * For exclusion constraints we just do the normal check, but now it's
     185                 :             :                  * okay to throw error.  In the HOT-update case, we must use the live
     186                 :             :                  * HOT child's TID here, else check_exclusion_constraint will think
     187                 :             :                  * the child is a conflict.
     188                 :             :                  */
     189                 :           8 :                 check_exclusion_constraint(trigdata->tg_relation, indexRel, indexInfo,
     190                 :           4 :                                                                    &tmptid, values, isnull,
     191                 :           4 :                                                                    estate, false);
     192                 :             :         }
     193                 :             : 
     194                 :             :         /*
     195                 :             :          * If that worked, then this index entry is unique or non-excluded, and we
     196                 :             :          * are done.
     197                 :             :          */
     198         [ +  + ]:          20 :         if (estate != NULL)
     199                 :           1 :                 FreeExecutorState(estate);
     200                 :             : 
     201                 :          20 :         ExecDropSingleTupleTableSlot(slot);
     202                 :             : 
     203                 :          20 :         index_close(indexRel, RowExclusiveLock);
     204                 :             : 
     205                 :          20 :         return PointerGetDatum(NULL);
     206                 :          20 : }
        

Generated by: LCOV version 2.3.2-1