LCOV - code coverage report
Current view: top level - contrib/tcn - tcn.c (source / functions) Hit Total Coverage
Test: PostgreSQL 19devel Lines: 57 67 85.1 %
Date: 2025-12-03 22:17:38 Functions: 4 4 100.0 %
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-2025, 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           4 : 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          30 : strcpy_quoted(StringInfo r, const char *s, const char q)
      37             : {
      38          30 :     appendStringInfoCharMacro(r, q);
      39         140 :     while (*s)
      40             :     {
      41         110 :         if (*s == q)
      42           0 :             appendStringInfoCharMacro(r, q);
      43         110 :         appendStringInfoCharMacro(r, *s);
      44         110 :         s++;
      45             :     }
      46          30 :     appendStringInfoCharMacro(r, q);
      47          30 : }
      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           4 : PG_FUNCTION_INFO_V1(triggered_change_notification);
      57             : 
      58             : Datum
      59          10 : triggered_change_notification(PG_FUNCTION_ARGS)
      60             : {
      61          10 :     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          10 :     initStringInfo(&payload);
      76             :     /* make sure it's called as a trigger */
      77          10 :     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          10 :     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          10 :     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          10 :     if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
      95           4 :         operation = 'I';
      96           6 :     else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
      97           2 :         operation = 'U';
      98           4 :     else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event))
      99           4 :         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          10 :     trigger = trigdata->tg_trigger;
     107          10 :     nargs = trigger->tgnargs;
     108          10 :     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          10 :     if (nargs == 0)
     114           0 :         channel = "tcn";
     115             :     else
     116          10 :         channel = trigger->tgargs[0];
     117             : 
     118             :     /* get tuple data */
     119          10 :     trigtuple = trigdata->tg_trigtuple;
     120          10 :     rel = trigdata->tg_relation;
     121          10 :     tupdesc = rel->rd_att;
     122             : 
     123          10 :     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          10 :     indexoidlist = RelationGetIndexList(rel);
     131             : 
     132          10 :     foreach(indexoidscan, indexoidlist)
     133             :     {
     134          10 :         Oid         indexoid = lfirst_oid(indexoidscan);
     135             :         HeapTuple   indexTuple;
     136             :         Form_pg_index index;
     137             : 
     138          10 :         indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid));
     139          10 :         if (!HeapTupleIsValid(indexTuple))  /* should not happen */
     140           0 :             elog(ERROR, "cache lookup failed for index %u", indexoid);
     141          10 :         index = (Form_pg_index) GETSTRUCT(indexTuple);
     142             :         /* we're only interested if it is the primary key and valid */
     143          10 :         if (index->indisprimary && index->indisvalid)
     144             :         {
     145          10 :             int         indnkeyatts = index->indnkeyatts;
     146             : 
     147          10 :             if (indnkeyatts > 0)
     148             :             {
     149             :                 int         i;
     150             : 
     151          10 :                 foundPK = true;
     152             : 
     153          10 :                 strcpy_quoted(&payload, RelationGetRelationName(rel), '"');
     154          10 :                 appendStringInfoCharMacro(&payload, ',');
     155          10 :                 appendStringInfoCharMacro(&payload, operation);
     156             : 
     157          20 :                 for (i = 0; i < indnkeyatts; i++)
     158             :                 {
     159          10 :                     int         colno = index->indkey.values[i];
     160          10 :                     Form_pg_attribute attr = TupleDescAttr(tupdesc, colno - 1);
     161             : 
     162          10 :                     appendStringInfoCharMacro(&payload, ',');
     163          10 :                     strcpy_quoted(&payload, NameStr(attr->attname), '"');
     164          10 :                     appendStringInfoCharMacro(&payload, '=');
     165          10 :                     strcpy_quoted(&payload, SPI_getvalue(trigtuple, tupdesc, colno), '\'');
     166             :                 }
     167             : 
     168          10 :                 Async_Notify(channel, payload.data);
     169             :             }
     170          10 :             ReleaseSysCache(indexTuple);
     171          10 :             break;
     172             :         }
     173           0 :         ReleaseSysCache(indexTuple);
     174             :     }
     175             : 
     176          10 :     list_free(indexoidlist);
     177             : 
     178          10 :     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          10 :     return PointerGetDatum(NULL);   /* after trigger; value doesn't matter */
     184             : }

Generated by: LCOV version 1.16