Age Owner Branch data TLA 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 : :
265 tgl@sss.pgh.pa.us 26 :CBC 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
5080 rhaas@postgresql.org 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)
5080 rhaas@postgresql.org 42 [ # # ]:UBC 0 : appendStringInfoCharMacro(r, q);
5080 rhaas@postgresql.org 43 [ - + ]:CBC 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 : :
40 drowley@postgresql.o 75 :GNC 5 : initStringInfo(&payload);
76 : : /* make sure it's called as a trigger */
5080 rhaas@postgresql.org 77 [ + - - + ]:CBC 5 : if (!CALLED_AS_TRIGGER(fcinfo))
5080 rhaas@postgresql.org 78 [ # # ]:UBC 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 */
5080 rhaas@postgresql.org 83 [ - + ]:CBC 5 : if (!TRIGGER_FIRED_AFTER(trigdata->tg_event))
5080 rhaas@postgresql.org 84 [ # # ]:UBC 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 */
5080 rhaas@postgresql.org 89 [ - + ]:CBC 5 : if (!TRIGGER_FIRED_FOR_ROW(trigdata->tg_event))
5080 rhaas@postgresql.org 90 [ # # ]:UBC 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 : :
5080 rhaas@postgresql.org 94 [ + + ]:CBC 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 : : {
5080 rhaas@postgresql.org 102 [ # # ]:UBC 0 : elog(ERROR, "triggered_change_notification: trigger fired by unrecognized operation");
103 : : operation = 'X'; /* silence compiler warning */
104 : : }
105 : :
5080 rhaas@postgresql.org 106 :CBC 5 : trigger = trigdata->tg_trigger;
107 : 5 : nargs = trigger->tgnargs;
108 [ - + ]: 5 : if (nargs > 1)
5080 rhaas@postgresql.org 109 [ # # ]:UBC 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 : :
5080 rhaas@postgresql.org 113 [ - + ]:CBC 5 : if (nargs == 0)
5080 rhaas@postgresql.org 114 :UBC 0 : channel = "tcn";
115 : : else
5080 rhaas@postgresql.org 116 :CBC 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));
3100 tgl@sss.pgh.pa.us 139 [ - + ]: 5 : if (!HeapTupleIsValid(indexTuple)) /* should not happen */
5080 rhaas@postgresql.org 140 [ # # ]:UBC 0 : elog(ERROR, "cache lookup failed for index %u", indexoid);
5080 rhaas@postgresql.org 141 :CBC 5 : index = (Form_pg_index) GETSTRUCT(indexTuple);
142 : : /* we're only interested if it is the primary key and valid */
2546 peter_e@gmx.net 143 [ + - + - ]: 5 : if (index->indisprimary && index->indisvalid)
144 : : {
2791 tgl@sss.pgh.pa.us 145 : 5 : int indnkeyatts = index->indnkeyatts;
146 : :
2810 teodor@sigaev.ru 147 [ + - ]: 5 : if (indnkeyatts > 0)
148 : : {
149 : : int i;
150 : :
5080 rhaas@postgresql.org 151 : 5 : foundPK = true;
152 : :
40 drowley@postgresql.o 153 :GNC 5 : strcpy_quoted(&payload, RelationGetRelationName(rel), '"');
154 [ - + ]: 5 : appendStringInfoCharMacro(&payload, ',');
155 [ - + ]: 5 : appendStringInfoCharMacro(&payload, operation);
156 : :
2810 teodor@sigaev.ru 157 [ + + ]:CBC 10 : for (i = 0; i < indnkeyatts; i++)
158 : : {
5080 rhaas@postgresql.org 159 : 5 : int colno = index->indkey.values[i];
3040 andres@anarazel.de 160 : 5 : Form_pg_attribute attr = TupleDescAttr(tupdesc, colno - 1);
161 : :
40 drowley@postgresql.o 162 [ - + ]:GNC 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 : : }
5080 rhaas@postgresql.org 170 :CBC 5 : ReleaseSysCache(indexTuple);
171 : 5 : break;
172 : : }
5080 rhaas@postgresql.org 173 :UBC 0 : ReleaseSysCache(indexTuple);
174 : : }
175 : :
5080 rhaas@postgresql.org 176 :CBC 5 : list_free(indexoidlist);
177 : :
178 [ - + ]: 5 : if (!foundPK)
5080 rhaas@postgresql.org 179 [ # # ]:UBC 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 : :
3100 tgl@sss.pgh.pa.us 183 :CBC 5 : return PointerGetDatum(NULL); /* after trigger; value doesn't matter */
184 : : }
|