LCOV - code coverage report
Current view: top level - contrib/tcn - tcn.c (source / functions) Coverage Total Hit
Test: PostgreSQL 19devel Lines: 85.1 % 67 57
Test Date: 2026-03-10 18:15:00 Functions: 100.0 % 4 4
Legend: Lines:     hit not hit

            Line data    Source code
       1              : /*-------------------------------------------------------------------------
       2              :  *
       3              :  * tcn.c
       4              :  *    triggered change notification support for PostgreSQL
       5              :  *
       6              :  * Portions Copyright (c) 2011-2026, PostgreSQL Global Development Group
       7              :  * Portions Copyright (c) 1994, Regents of the University of California
       8              :  *
       9              :  *
      10              :  * IDENTIFICATION
      11              :  *    contrib/tcn/tcn.c
      12              :  *
      13              :  *-------------------------------------------------------------------------
      14              :  */
      15              : 
      16              : #include "postgres.h"
      17              : 
      18              : #include "access/htup_details.h"
      19              : #include "commands/async.h"
      20              : #include "commands/trigger.h"
      21              : #include "executor/spi.h"
      22              : #include "lib/stringinfo.h"
      23              : #include "utils/rel.h"
      24              : #include "utils/syscache.h"
      25              : 
      26            2 : PG_MODULE_MAGIC_EXT(
      27              :                     .name = "tcn",
      28              :                     .version = PG_VERSION
      29              : );
      30              : 
      31              : /*
      32              :  * Copy from s (for source) to r (for result), wrapping with q (quote)
      33              :  * characters and doubling any quote characters found.
      34              :  */
      35              : static void
      36           15 : strcpy_quoted(StringInfo r, const char *s, const char q)
      37              : {
      38           15 :     appendStringInfoCharMacro(r, q);
      39           70 :     while (*s)
      40              :     {
      41           55 :         if (*s == q)
      42            0 :             appendStringInfoCharMacro(r, q);
      43           55 :         appendStringInfoCharMacro(r, *s);
      44           55 :         s++;
      45              :     }
      46           15 :     appendStringInfoCharMacro(r, q);
      47           15 : }
      48              : 
      49              : /*
      50              :  * triggered_change_notification
      51              :  *
      52              :  * This trigger function will send a notification of data modification with
      53              :  * primary key values.  The channel will be "tcn" unless the trigger is
      54              :  * created with a parameter, in which case that parameter will be used.
      55              :  */
      56            2 : PG_FUNCTION_INFO_V1(triggered_change_notification);
      57              : 
      58              : Datum
      59            5 : triggered_change_notification(PG_FUNCTION_ARGS)
      60              : {
      61            5 :     TriggerData *trigdata = (TriggerData *) fcinfo->context;
      62              :     Trigger    *trigger;
      63              :     int         nargs;
      64              :     HeapTuple   trigtuple;
      65              :     Relation    rel;
      66              :     TupleDesc   tupdesc;
      67              :     char       *channel;
      68              :     char        operation;
      69              :     StringInfoData payload;
      70              :     bool        foundPK;
      71              : 
      72              :     List       *indexoidlist;
      73              :     ListCell   *indexoidscan;
      74              : 
      75            5 :     initStringInfo(&payload);
      76              :     /* make sure it's called as a trigger */
      77            5 :     if (!CALLED_AS_TRIGGER(fcinfo))
      78            0 :         ereport(ERROR,
      79              :                 (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
      80              :                  errmsg("triggered_change_notification: must be called as trigger")));
      81              : 
      82              :     /* and that it's called after the change */
      83            5 :     if (!TRIGGER_FIRED_AFTER(trigdata->tg_event))
      84            0 :         ereport(ERROR,
      85              :                 (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
      86              :                  errmsg("triggered_change_notification: must be called after the change")));
      87              : 
      88              :     /* and that it's called for each row */
      89            5 :     if (!TRIGGER_FIRED_FOR_ROW(trigdata->tg_event))
      90            0 :         ereport(ERROR,
      91              :                 (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
      92              :                  errmsg("triggered_change_notification: must be called for each row")));
      93              : 
      94            5 :     if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
      95            2 :         operation = 'I';
      96            3 :     else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
      97            1 :         operation = 'U';
      98            2 :     else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event))
      99            2 :         operation = 'D';
     100              :     else
     101              :     {
     102            0 :         elog(ERROR, "triggered_change_notification: trigger fired by unrecognized operation");
     103              :         operation = 'X';        /* silence compiler warning */
     104              :     }
     105              : 
     106            5 :     trigger = trigdata->tg_trigger;
     107            5 :     nargs = trigger->tgnargs;
     108            5 :     if (nargs > 1)
     109            0 :         ereport(ERROR,
     110              :                 (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
     111              :                  errmsg("triggered_change_notification: must not be called with more than one parameter")));
     112              : 
     113            5 :     if (nargs == 0)
     114            0 :         channel = "tcn";
     115              :     else
     116            5 :         channel = trigger->tgargs[0];
     117              : 
     118              :     /* get tuple data */
     119            5 :     trigtuple = trigdata->tg_trigtuple;
     120            5 :     rel = trigdata->tg_relation;
     121            5 :     tupdesc = rel->rd_att;
     122              : 
     123            5 :     foundPK = false;
     124              : 
     125              :     /*
     126              :      * Get the list of index OIDs for the table from the relcache, and look up
     127              :      * each one in the pg_index syscache until we find one marked primary key
     128              :      * (hopefully there isn't more than one such).
     129              :      */
     130            5 :     indexoidlist = RelationGetIndexList(rel);
     131              : 
     132            5 :     foreach(indexoidscan, indexoidlist)
     133              :     {
     134            5 :         Oid         indexoid = lfirst_oid(indexoidscan);
     135              :         HeapTuple   indexTuple;
     136              :         Form_pg_index index;
     137              : 
     138            5 :         indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid));
     139            5 :         if (!HeapTupleIsValid(indexTuple))  /* should not happen */
     140            0 :             elog(ERROR, "cache lookup failed for index %u", indexoid);
     141            5 :         index = (Form_pg_index) GETSTRUCT(indexTuple);
     142              :         /* we're only interested if it is the primary key and valid */
     143            5 :         if (index->indisprimary && index->indisvalid)
     144              :         {
     145            5 :             int         indnkeyatts = index->indnkeyatts;
     146              : 
     147            5 :             if (indnkeyatts > 0)
     148              :             {
     149              :                 int         i;
     150              : 
     151            5 :                 foundPK = true;
     152              : 
     153            5 :                 strcpy_quoted(&payload, RelationGetRelationName(rel), '"');
     154            5 :                 appendStringInfoCharMacro(&payload, ',');
     155            5 :                 appendStringInfoCharMacro(&payload, operation);
     156              : 
     157           10 :                 for (i = 0; i < indnkeyatts; i++)
     158              :                 {
     159            5 :                     int         colno = index->indkey.values[i];
     160            5 :                     Form_pg_attribute attr = TupleDescAttr(tupdesc, colno - 1);
     161              : 
     162            5 :                     appendStringInfoCharMacro(&payload, ',');
     163            5 :                     strcpy_quoted(&payload, NameStr(attr->attname), '"');
     164            5 :                     appendStringInfoCharMacro(&payload, '=');
     165            5 :                     strcpy_quoted(&payload, SPI_getvalue(trigtuple, tupdesc, colno), '\'');
     166              :                 }
     167              : 
     168            5 :                 Async_Notify(channel, payload.data);
     169              :             }
     170            5 :             ReleaseSysCache(indexTuple);
     171            5 :             break;
     172              :         }
     173            0 :         ReleaseSysCache(indexTuple);
     174              :     }
     175              : 
     176            5 :     list_free(indexoidlist);
     177              : 
     178            5 :     if (!foundPK)
     179            0 :         ereport(ERROR,
     180              :                 (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
     181              :                  errmsg("triggered_change_notification: must be called on a table with a primary key")));
     182              : 
     183            5 :     return PointerGetDatum(NULL);   /* after trigger; value doesn't matter */
     184              : }
        

Generated by: LCOV version 2.0-1