]> granicus.if.org Git - postgresql/commitdiff
Triggered change notifications.
authorRobert Haas <rhaas@postgresql.org>
Fri, 20 Jan 2012 04:15:15 +0000 (23:15 -0500)
committerRobert Haas <rhaas@postgresql.org>
Fri, 20 Jan 2012 04:15:15 +0000 (23:15 -0500)
Kevin Grittner, reviewed (in earlier versions) by Álvaro Herrera

contrib/Makefile
contrib/tcn/Makefile [new file with mode: 0644]
contrib/tcn/tcn--1.0.sql [new file with mode: 0644]
contrib/tcn/tcn.c [new file with mode: 0644]
contrib/tcn/tcn.control [new file with mode: 0644]
doc/src/sgml/contrib.sgml
doc/src/sgml/filelist.sgml
doc/src/sgml/tcn.sgml [new file with mode: 0644]

index 0c238aae16c2939411bcace2e02a0ac9dabc4e7b..ac0a80a014059288acd39bb3175338cd5ef439d8 100644 (file)
@@ -45,6 +45,7 @@ SUBDIRS = \
                seg             \
                spi             \
                tablefunc       \
+               tcn             \
                test_parser     \
                tsearch2        \
                unaccent        \
diff --git a/contrib/tcn/Makefile b/contrib/tcn/Makefile
new file mode 100644 (file)
index 0000000..7bac5e3
--- /dev/null
@@ -0,0 +1,17 @@
+# contrib/tcn/Makefile
+
+MODULES = tcn
+
+EXTENSION = tcn
+DATA = tcn--1.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/tcn
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/tcn/tcn--1.0.sql b/contrib/tcn/tcn--1.0.sql
new file mode 100644 (file)
index 0000000..027a4ef
--- /dev/null
@@ -0,0 +1,9 @@
+/* contrib/tcn/tcn--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION tcn" to load this file. \quit
+
+CREATE FUNCTION triggered_change_notification()
+RETURNS pg_catalog.trigger
+AS 'MODULE_PATHNAME'
+LANGUAGE C;
diff --git a/contrib/tcn/tcn.c b/contrib/tcn/tcn.c
new file mode 100644 (file)
index 0000000..314632d
--- /dev/null
@@ -0,0 +1,184 @@
+/*-------------------------------------------------------------------------
+ *
+ * tcn.c
+ *       triggered change notification support for PostgreSQL
+ *
+ * Portions Copyright (c) 2011-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *       contrib/tcn/tcn.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "executor/spi.h"
+#include "commands/async.h"
+#include "commands/trigger.h"
+#include "lib/stringinfo.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+
+PG_MODULE_MAGIC;
+
+
+/* forward declarations */
+Datum          triggered_change_notification(PG_FUNCTION_ARGS);
+
+
+/*
+ * Copy from s (for source) to r (for result), wrapping with q (quote)
+ * characters and doubling any quote characters found.
+ */
+static void
+strcpy_quoted(StringInfo r, const char *s, const char q)
+{
+       appendStringInfoCharMacro(r, q);
+       while (*s)
+       {
+               if (*s == q)
+                       appendStringInfoCharMacro(r, q);
+               appendStringInfoCharMacro(r, *s);
+               s++;
+       }
+       appendStringInfoCharMacro(r, q);
+}
+
+/*
+ * triggered_change_notification
+ *
+ * This trigger function will send a notification of data modification with
+ * primary key values. The channel will be "tcn" unless the trigger is
+ * created with a parameter, in which case that parameter will be used.
+ */
+PG_FUNCTION_INFO_V1(triggered_change_notification);
+
+Datum
+triggered_change_notification(PG_FUNCTION_ARGS)
+{
+       TriggerData *trigdata = (TriggerData *) fcinfo->context;
+       Trigger    *trigger;
+       int                     nargs;
+       HeapTuple       trigtuple;
+       Relation        rel;
+       TupleDesc       tupdesc;
+       char       *channel;
+       char            operation;
+       StringInfo      payload = makeStringInfo();
+       bool            foundPK;
+
+       List       *indexoidlist;
+       ListCell   *indexoidscan;
+
+       /* make sure it's called as a trigger */
+       if (!CALLED_AS_TRIGGER(fcinfo))
+               ereport(ERROR,
+                               (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
+               errmsg("triggered_change_notification: must be called as trigger")));
+
+       /* and that it's called after the change */
+       if (!TRIGGER_FIRED_AFTER(trigdata->tg_event))
+               ereport(ERROR,
+                               (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
+                                errmsg("triggered_change_notification: must be called after the change")));
+
+       /* and that it's called for each row */
+       if (!TRIGGER_FIRED_FOR_ROW(trigdata->tg_event))
+               ereport(ERROR,
+                               (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
+                                errmsg("triggered_change_notification: must be called for each row")));
+
+       if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
+               operation = 'I';
+       else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
+               operation = 'U';
+       else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event))
+               operation = 'D';
+       else
+       {
+               elog(ERROR, "triggered_change_notification: trigger fired by unrecognized operation");
+               operation = 'X';                /* silence compiler warning */
+       }
+
+       trigger = trigdata->tg_trigger;
+       nargs = trigger->tgnargs;
+       if (nargs > 1)
+               ereport(ERROR,
+                               (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
+                                errmsg("triggered_change_notification: must not be called with more than one parameter")));
+
+       if (nargs == 0)
+               channel = "tcn";
+       else
+               channel = trigger->tgargs[0];
+
+       /* get tuple data */
+       trigtuple = trigdata->tg_trigtuple;
+       rel = trigdata->tg_relation;
+       tupdesc = rel->rd_att;
+
+       foundPK = false;
+
+       /*
+        * Get the list of index OIDs for the table from the relcache, and look up
+        * each one in the pg_index syscache until we find one marked primary key
+        * (hopefully there isn't more than one such).
+        */
+       indexoidlist = RelationGetIndexList(rel);
+
+       foreach(indexoidscan, indexoidlist)
+       {
+               Oid                     indexoid = lfirst_oid(indexoidscan);
+               HeapTuple       indexTuple;
+               Form_pg_index index;
+
+               indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid));
+               if (!HeapTupleIsValid(indexTuple))              /* should not happen */
+                       elog(ERROR, "cache lookup failed for index %u", indexoid);
+               index = (Form_pg_index) GETSTRUCT(indexTuple);
+               /* we're only interested if it is the primary key */
+               if (index->indisprimary)
+               {
+                       int                     numatts = index->indnatts;
+
+                       if (numatts > 0)
+                       {
+                               int                     i;
+
+                               foundPK = true;
+
+                               strcpy_quoted(payload, RelationGetRelationName(rel), '"');
+                               appendStringInfoCharMacro(payload, ',');
+                               appendStringInfoCharMacro(payload, operation);
+
+                               for (i = 0; i < numatts; i++)
+                               {
+                                       int                     colno = index->indkey.values[i];
+
+                                       appendStringInfoCharMacro(payload, ',');
+                                       strcpy_quoted(payload, NameStr((tupdesc->attrs[colno - 1])->attname), '"');
+                                       appendStringInfoCharMacro(payload, '=');
+                                       strcpy_quoted(payload, SPI_getvalue(trigtuple, tupdesc, colno), '\'');
+                               }
+
+                               Async_Notify(channel, payload->data);
+                       }
+                       ReleaseSysCache(indexTuple);
+                       break;
+               }
+               ReleaseSysCache(indexTuple);
+       }
+
+       list_free(indexoidlist);
+
+       if (!foundPK)
+               ereport(ERROR,
+                               (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
+                                errmsg("triggered_change_notification: must be called on a table with a primary key")));
+
+       return PointerGetDatum(NULL);           /* after trigger; value doesn't matter */
+}
diff --git a/contrib/tcn/tcn.control b/contrib/tcn/tcn.control
new file mode 100644 (file)
index 0000000..8abfd19
--- /dev/null
@@ -0,0 +1,5 @@
+# tcn extension
+comment = 'Triggered change notifications'
+default_version = '1.0'
+module_pathname = '$libdir/tcn'
+relocatable = true
index adf09ca872d99af973dc36cbc209b673a74db9d6..d4da4eec87d9c8e5d7ec550e70ecd3473d5adc59 100644 (file)
@@ -128,6 +128,7 @@ CREATE EXTENSION <replaceable>module_name</> FROM unpackaged;
  &contrib-spi;
  &sslinfo;
  &tablefunc;
+ &tcn;
  &test-parser;
  &tsearch2;
  &unaccent;
index b96dd656ad3ad44692e46851d15f1931ceb2d868..b5d3c6d4fce27ffcc56162cc283f4e032312ca51 100644 (file)
 <!ENTITY sepgsql         SYSTEM "sepgsql.sgml">
 <!ENTITY sslinfo         SYSTEM "sslinfo.sgml">
 <!ENTITY tablefunc       SYSTEM "tablefunc.sgml">
+<!ENTITY tcn             SYSTEM "tcn.sgml">
 <!ENTITY test-parser     SYSTEM "test-parser.sgml">
 <!ENTITY tsearch2        SYSTEM "tsearch2.sgml">
 <!ENTITY unaccent      SYSTEM "unaccent.sgml">
diff --git a/doc/src/sgml/tcn.sgml b/doc/src/sgml/tcn.sgml
new file mode 100644 (file)
index 0000000..af830df
--- /dev/null
@@ -0,0 +1,72 @@
+<!-- doc/src/sgml/tcn.sgml -->
+
+<sect1 id="tcn" xreflabel="tcn">
+ <title>tcn</title>
+
+ <indexterm zone="tcn">
+  <primary>tcn</primary>
+ </indexterm>
+
+ <indexterm zone="tcn">
+  <primary>triggered_change_notification</primary>
+ </indexterm>
+
+ <para>
+  The <filename>tcn</> module provides a trigger function that notifies
+  listeners of changes to any table on which it is attached.  It must be
+  used as an <literal>AFTER</> trigger <literal>FOR EACH ROW</>.
+ </para>
+
+ <para>
+  Only one parameter may be suupplied to the function in a
+  <literal>CREATE TRIGGER</> statement, and that is optional.  If supplied
+  it will be used for the channel name for the notifications.  If omitted
+  <literal>tcn</> will be used for the channel name.
+ </para>
+
+ <para>
+  The payload of the notifications consists of the table name, a letter to
+  indicate which type of operation was performed, and column name/value pairs
+  for primary key columns.  Each part is separated from the next by a comma.
+  For ease of parsing using regular expressions, table and column names are
+  always wrapped in double quotes, and data values are always wrapped in
+  single quotes.  Embeded quotes are doubled.
+ </para>
+
+ <para>
+  A brief example of using the extension follows.
+
+<programlisting>
+test=# create table tcndata
+test-#   (
+test(#     a int not null,
+test(#     b date not null,
+test(#     c text,
+test(#     primary key (a, b)
+test(#   );
+NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "tcndata_pkey" for table "tcndata"
+CREATE TABLE
+test=# create trigger tcndata_tcn_trigger
+test-#   after insert or update or delete on tcndata
+test-#   for each row execute procedure triggered_change_notification();
+CREATE TRIGGER
+test=# listen tcn;
+LISTEN
+test=# insert into tcndata values (1, date '2012-12-22', 'one'),
+test-#                            (1, date '2012-12-23', 'another'),
+test-#                            (2, date '2012-12-23', 'two');
+INSERT 0 3
+Asynchronous notification "tcn" with payload ""tcndata",I,"a"='1',"b"='2012-12-22'" received from server process with PID 22770.
+Asynchronous notification "tcn" with payload ""tcndata",I,"a"='1',"b"='2012-12-23'" received from server process with PID 22770.
+Asynchronous notification "tcn" with payload ""tcndata",I,"a"='2',"b"='2012-12-23'" received from server process with PID 22770.
+test=# update tcndata set c = 'uno' where a = 1;
+UPDATE 2
+Asynchronous notification "tcn" with payload ""tcndata",U,"a"='1',"b"='2012-12-22'" received from server process with PID 22770.
+Asynchronous notification "tcn" with payload ""tcndata",U,"a"='1',"b"='2012-12-23'" received from server process with PID 22770.
+test=# delete from tcndata where a = 1 and b = date '2012-12-22';
+DELETE 1
+Asynchronous notification "tcn" with payload ""tcndata",D,"a"='1',"b"='2012-12-22'" received from server process with PID 22770.
+</programlisting>
+ </para>
+
+</sect1>