summaryrefslogtreecommitdiffstats
path: root/contrib/tcn
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/tcn')
-rw-r--r--contrib/tcn/.gitignore6
-rw-r--r--contrib/tcn/Makefile21
-rw-r--r--contrib/tcn/expected/tcn.out13
-rw-r--r--contrib/tcn/meson.build35
-rw-r--r--contrib/tcn/specs/tcn.spec28
-rw-r--r--contrib/tcn/tcn--1.0.sql9
-rw-r--r--contrib/tcn/tcn.c180
-rw-r--r--contrib/tcn/tcn.control6
8 files changed, 298 insertions, 0 deletions
diff --git a/contrib/tcn/.gitignore b/contrib/tcn/.gitignore
new file mode 100644
index 0000000..b4903eb
--- /dev/null
+++ b/contrib/tcn/.gitignore
@@ -0,0 +1,6 @@
+# Generated subdirectories
+/log/
+/results/
+/output_iso/
+/tmp_check/
+/tmp_check_iso/
diff --git a/contrib/tcn/Makefile b/contrib/tcn/Makefile
new file mode 100644
index 0000000..6813289
--- /dev/null
+++ b/contrib/tcn/Makefile
@@ -0,0 +1,21 @@
+# contrib/tcn/Makefile
+
+MODULES = tcn
+
+EXTENSION = tcn
+DATA = tcn--1.0.sql
+PGFILEDESC = "tcn - trigger function notifying listeners"
+
+ISOLATION = tcn
+ISOLATION_OPTS = --load-extension=tcn
+
+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/expected/tcn.out b/contrib/tcn/expected/tcn.out
new file mode 100644
index 0000000..8c67113
--- /dev/null
+++ b/contrib/tcn/expected/tcn.out
@@ -0,0 +1,13 @@
+Parsed test spec with 1 sessions
+
+starting permutation: listen insert insert2 update delete
+step listen: LISTEN mychannel;
+step insert: INSERT INTO mytable VALUES(1, 'one');
+s1: NOTIFY "mychannel" with payload ""mytable",I,"key"='1'" from s1
+step insert2: INSERT INTO mytable VALUES(2, 'two');
+s1: NOTIFY "mychannel" with payload ""mytable",I,"key"='2'" from s1
+step update: UPDATE mytable SET value = 'foo' WHERE key = 2;
+s1: NOTIFY "mychannel" with payload ""mytable",U,"key"='2'" from s1
+step delete: DELETE FROM mytable;
+s1: NOTIFY "mychannel" with payload ""mytable",D,"key"='1'" from s1
+s1: NOTIFY "mychannel" with payload ""mytable",D,"key"='2'" from s1
diff --git a/contrib/tcn/meson.build b/contrib/tcn/meson.build
new file mode 100644
index 0000000..3028bc5
--- /dev/null
+++ b/contrib/tcn/meson.build
@@ -0,0 +1,35 @@
+# Copyright (c) 2022-2023, PostgreSQL Global Development Group
+
+tcn_sources = files(
+ 'tcn.c',
+)
+
+if host_system == 'windows'
+ tcn_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+ '--NAME', 'tcn',
+ '--FILEDESC', 'tcn - trigger function notifying listeners',])
+endif
+
+tcn = shared_module('tcn',
+ tcn_sources,
+ kwargs: contrib_mod_args,
+)
+contrib_targets += tcn
+
+install_data(
+ 'tcn--1.0.sql',
+ 'tcn.control',
+ kwargs: contrib_data_args,
+)
+
+tests += {
+ 'name': 'tcn',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'isolation': {
+ 'specs': [
+ 'tcn',
+ ],
+ 'regress_args': ['--load-extension=tcn'],
+ },
+}
diff --git a/contrib/tcn/specs/tcn.spec b/contrib/tcn/specs/tcn.spec
new file mode 100644
index 0000000..fb9e730
--- /dev/null
+++ b/contrib/tcn/specs/tcn.spec
@@ -0,0 +1,28 @@
+# Tests for contrib/tcn
+
+# These tests use only self-notifies within a single session,
+# which are convenient because they minimize timing concerns.
+# Whether the NOTIFY mechanism works across sessions is not
+# really tcn's problem.
+
+setup
+{
+ CREATE TABLE mytable (key int PRIMARY KEY, value text);
+ CREATE TRIGGER tcntrig AFTER INSERT OR UPDATE OR DELETE ON mytable
+ FOR EACH ROW EXECUTE FUNCTION triggered_change_notification(mychannel);
+}
+
+teardown
+{
+ DROP TABLE mytable;
+}
+
+session s1
+step listen { LISTEN mychannel; }
+step insert { INSERT INTO mytable VALUES(1, 'one'); }
+step insert2 { INSERT INTO mytable VALUES(2, 'two'); }
+step update { UPDATE mytable SET value = 'foo' WHERE key = 2; }
+step delete { DELETE FROM mytable; }
+
+
+permutation listen insert insert2 update delete
diff --git a/contrib/tcn/tcn--1.0.sql b/contrib/tcn/tcn--1.0.sql
new file mode 100644
index 0000000..027a4ef
--- /dev/null
+++ b/contrib/tcn/tcn--1.0.sql
@@ -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
index 0000000..546fbf2
--- /dev/null
+++ b/contrib/tcn/tcn.c
@@ -0,0 +1,180 @@
+/*-------------------------------------------------------------------------
+ *
+ * tcn.c
+ * triggered change notification support for PostgreSQL
+ *
+ * Portions Copyright (c) 2011-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * contrib/tcn/tcn.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "commands/async.h"
+#include "commands/trigger.h"
+#include "executor/spi.h"
+#include "lib/stringinfo.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+PG_MODULE_MAGIC;
+
+/*
+ * 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 and valid */
+ if (index->indisprimary && index->indisvalid)
+ {
+ int indnkeyatts = index->indnkeyatts;
+
+ if (indnkeyatts > 0)
+ {
+ int i;
+
+ foundPK = true;
+
+ strcpy_quoted(payload, RelationGetRelationName(rel), '"');
+ appendStringInfoCharMacro(payload, ',');
+ appendStringInfoCharMacro(payload, operation);
+
+ for (i = 0; i < indnkeyatts; i++)
+ {
+ int colno = index->indkey.values[i];
+ Form_pg_attribute attr = TupleDescAttr(tupdesc, colno - 1);
+
+ appendStringInfoCharMacro(payload, ',');
+ strcpy_quoted(payload, NameStr(attr->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
index 0000000..6972e11
--- /dev/null
+++ b/contrib/tcn/tcn.control
@@ -0,0 +1,6 @@
+# tcn extension
+comment = 'Triggered change notifications'
+default_version = '1.0'
+module_pathname = '$libdir/tcn'
+relocatable = true
+trusted = true