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