diff --git a/contrib/statement_trigger_row/Makefile b/contrib/statement_trigger_row/Makefile new file mode 100644 index 0000000..e0cf006 --- /dev/null +++ b/contrib/statement_trigger_row/Makefile @@ -0,0 +1,17 @@ +# contrib/statement_trigger_row/Makefile + +MODULES = statement_trigger_row + +EXTENSION = statement_trigger_row +DATA = statement_trigger_row--1.0.sql + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = contrib/statement_trigger_row +top_builddir = ../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/contrib/statement_trigger_row/sql/easy_way.sql b/contrib/statement_trigger_row/sql/easy_way.sql new file mode 100644 index 0000000..019ae7f --- /dev/null +++ b/contrib/statement_trigger_row/sql/easy_way.sql @@ -0,0 +1,85 @@ +/* + * If these were surfaced to PL/pgsql, this is what it might look like. + */ + +BEGIN; + +CREATE TABLE a( + id SERIAL PRIMARY KEY, + i INT +); + +CREATE FUNCTION summarize_a_inserts() +RETURNS TRIGGER LANGUAGE plpgsql +AS $$ +DECLARE + the_sum BIGINT; +BEGIN + SELECT INTO the_sum sum(NEW.i) + FROM + new_a; + RAISE NOTICE 'Total change: %.', the_sum; + RETURN NULL; +END; +$$; + +CREATE FUNCTION summarize_a_updates() +RETURNS TRIGGER LANGUAGE plpgsql +AS $$ +DECLARE + the_sum BIGINT; +BEGIN + SELECT INTO the_sum sum(COALESCE(NEW.i,0) - COALESCE(OLD.i, 0)) + FROM + old_a + JOIN + new_a + ON(old_a.id = new_a.id); + RAISE NOTICE 'Total change: %.', the_sum; + RETURN NULL; +END; +$$; + +CREATE FUNCTION summarize_a_deletes() +RETURNS TRIGGER LANGUAGE plpgsql +AS $$ +DECLARE + the_sum BIGINT; +BEGIN + SELECT INTO the_sum -1 * sum(OLD.i) + FROM + old_a; + RAISE NOTICE 'Total change: %.', the_sum; + RETURN NULL; +END; +$$; + +CREATE TRIGGER statement_after_insert_a + AFTER INSERT ON a + REFERENCING + NEW TABLE AS new_a + FOR EACH STATEMENT + EXECUTE PROCEDURE summarize_a_inserts(); + +CREATE TRIGGER statement_after_update_a + AFTER UPDATE ON a + REFERENCING + OLD TABLE AS old_a + NEW TABLE AS new_a + FOR EACH STATEMENT + EXECUTE PROCEDURE summarize_a_updates(); + +CREATE TRIGGER statement_after_delete_a + AFTER DELETE ON a + REFERENCING + OLD TABLE AS old_a + FOR EACH STATEMENT + EXECUTE PROCEDURE summarize_a_deletes(); + +INSERT INTO a(i) +SELECT * FROM generate_series(1,10000); + +UPDATE a SET i=i+1; + +ROLLBACK; + diff --git a/contrib/statement_trigger_row/sql/hard_way.sql b/contrib/statement_trigger_row/sql/hard_way.sql new file mode 100644 index 0000000..c6f7c1d --- /dev/null +++ b/contrib/statement_trigger_row/sql/hard_way.sql @@ -0,0 +1,68 @@ +CREATE TABLE IF NOT EXISTS h( + i INTEGER +); + +CREATE FUNCTION set_up_h_rows() +RETURNS TRIGGER LANGUAGE plpgsql +AS $$ +BEGIN + CREATE TEMPORARY TABLE IF NOT EXISTS h_rows(LIKE a) ON COMMIT DROP; + RETURN NULL; +END; +$$; + +CREATE TRIGGER statement_before_writing_h + BEFORE INSERT OR UPDATE OR DELETE ON a + FOR EACH STATEMENT + EXECUTE PROCEDURE set_up_h_rows(); + +CREATE OR REPLACE FUNCTION stash_h_row_deltas() +RETURNS TRIGGER +LANGUAGE plpgsql +AS $$ +BEGIN + INSERT INTO h_rows(i) + VALUES( + CASE TG_OP + WHEN 'INSERT' THEN COALESCE(NEW.i,0) + WHEN 'UPDATE' THEN COALESCE(NEW.i,0) - COALESCE(OLD.i,0) + WHEN 'DELETE' THEN -1 * COALESCE(OLD.i,0) + END + ); + IF TG_OP IN ('INSERT','UPDATE') + THEN + RETURN NEW; + ELSE + RETURN OLD; + END IF; +END; +$$; + +CREATE TRIGGER during_trg + BEFORE INSERT OR UPDATE OR DELETE ON a + FOR EACH ROW + EXECUTE PROCEDURE stash_h_row_deltas(); + +CREATE FUNCTION summarize_h_rows() +RETURNS TRIGGER LANGUAGE plpgsql +AS $$ +DECLARE the_sum BIGINT; +BEGIN + SELECT INTO the_sum sum(i) FROM h_rows; + RAISE NOTICE 'Total change: %.', the_sum; + TRUNCATE h_rows; + RETURN NULL; +END; +$$; + +CREATE TRIGGER statement_after_writing_h + AFTER INSERT OR UPDATE OR DELETE ON a + FOR EACH STATEMENT + EXECUTE PROCEDURE summarize_h_rows(); + +INSERT INTO h(i) +SELECT * FROM generate_series(1,10000); + +UPDATE h SET i=i+1; + +DELETE FROM h WHERE i < 5000; diff --git a/contrib/statement_trigger_row/sql/statement_trigger_row.sql b/contrib/statement_trigger_row/sql/statement_trigger_row.sql new file mode 100644 index 0000000..4d5925a --- /dev/null +++ b/contrib/statement_trigger_row/sql/statement_trigger_row.sql @@ -0,0 +1,18 @@ +CREATE TABLE IF NOT EXISTS e( + i INT +); + +CREATE TRIGGER statement_dml_e + AFTER INSERT OR UPDATE OR DELETE ON e + REFERENCING + OLD TABLE AS old_e + NEW TABLE AS new_e + FOR EACH STATEMENT + EXECUTE PROCEDURE statement_trigger_row(); + +INSERT INTO e(i) +SELECT * FROM generate_series(1,10000); + +UPDATE e SET i=i+1; + +DELETE FROM e WHERE i < 5000; diff --git a/contrib/statement_trigger_row/statement_trigger_row--1.0.sql b/contrib/statement_trigger_row/statement_trigger_row--1.0.sql new file mode 100644 index 0000000..ffc0e36 --- /dev/null +++ b/contrib/statement_trigger_row/statement_trigger_row--1.0.sql @@ -0,0 +1,10 @@ +/* contrib/statement_trigger_row--1.0.sql */ + + +-- Complain if script is sourced in psql, rather than via CREATE EXTENSION. +\echo Use "CREATE EXTENSION statement_trigger_row" to load this file. \quit + +CREATE FUNCTION statement_trigger_row() +RETURNS pg_catalog.trigger +AS 'MODULE_PATHNAME' +LANGUAGE C; diff --git a/contrib/statement_trigger_row/statement_trigger_row.c b/contrib/statement_trigger_row/statement_trigger_row.c new file mode 100644 index 0000000..048eb3b --- /dev/null +++ b/contrib/statement_trigger_row/statement_trigger_row.c @@ -0,0 +1,140 @@ +/*------------------------------------------------------------------------- + * + * statement_trigger_row.c + * statement_trigger_row support for PostgreSQL + * + * Portions Copyright (c) 2011-2014, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * contrib/statement_trigger_row.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "executor/spi.h" +#include "commands/trigger.h" +#include "utils/rel.h" + +#ifdef PG_MODULE_MAGIC +PG_MODULE_MAGIC; +#endif + +extern Datum statement_trigger_row(PG_FUNCTION_ARGS); + +PG_FUNCTION_INFO_V1(statement_trigger_row); + +Datum +statement_trigger_row(PG_FUNCTION_ARGS) +{ + TriggerData *trigdata = (TriggerData *) fcinfo->context; + TupleDesc tupdesc; + TupleTableSlot *slot; + Tuplestorestate *new_tuplestore; + Tuplestorestate *old_tuplestore; + int64 delta = 0; + + if (!CALLED_AS_TRIGGER(fcinfo)) + { + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("statement_trigger_row: not called by trigger manager"))); + } + + if (!TRIGGER_FIRED_AFTER(trigdata->tg_event)) + { + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("statement_trigger_row: not called by AFTER trigger"))); + } + + if (TRIGGER_FIRED_BY_TRUNCATE(trigdata->tg_event)) + { + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("You may not call this function in a TRUNCATE trigger."))); + } + + tupdesc = trigdata->tg_relation->rd_att; + + slot = MakeSingleTupleTableSlot(tupdesc); + + if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event)) + { + if (trigdata->tg_newdelta == NULL) + ereport(ERROR, + (errmsg("You must include NEW TABLE AS in your CREATE TRIGGER statement"))); + + new_tuplestore = trigdata->tg_newdelta; + /* + * Ensure that we're at the right place in the tuplestore, as + * other triggers may have messed with the state. + */ + tuplestore_rescan(new_tuplestore); + + /* Iterate through the new tuples, adding. */ + while (tuplestore_gettupleslot(new_tuplestore, true, false, slot)) { + bool isnull; + Datum val = slot_getattr(slot, 1, &isnull); + if (!isnull) + delta += DatumGetInt32(val); + } + } + else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event)) + { + if (trigdata->tg_olddelta == NULL) + ereport(ERROR, + (errmsg("You must include OLD TABLE AS in your CREATE TRIGGER statement"))); + + old_tuplestore = trigdata->tg_olddelta; + tuplestore_rescan(old_tuplestore); + /* Iterate through the old tuples, subtracting. */ + while (tuplestore_gettupleslot(old_tuplestore, true, false, slot)) { + bool isnull; + Datum val = slot_getattr(slot, 1, &isnull); + if (!isnull) + delta -= DatumGetInt32(val); + } + } + else /* It's an UPDATE */ + { + if (trigdata->tg_olddelta == NULL) + ereport(ERROR, + (errmsg("You must include OLD TABLE AS in your CREATE TRIGGER statement"))); + if (trigdata->tg_newdelta == NULL) + ereport(ERROR, + (errmsg("You must include NEW TABLE AS in your CREATE TRIGGER statement"))); + + old_tuplestore = trigdata->tg_olddelta; + new_tuplestore = trigdata->tg_newdelta; + + tuplestore_rescan(old_tuplestore); + tuplestore_rescan(new_tuplestore); + + /* Iterate through both the new and old tuples, incrementing + * or decrementing as needed. */ + while (tuplestore_gettupleslot(new_tuplestore, true, false, slot)) { + bool isnull; + Datum val = slot_getattr(slot, 1, &isnull); + if (!isnull) + delta += DatumGetInt32(val); + } + + while (tuplestore_gettupleslot(old_tuplestore, true, false, slot)) { + bool isnull; + Datum val = slot_getattr(slot, 1, &isnull); + if (!isnull) + delta -= DatumGetInt32(val); + } + + } + + ExecDropSingleTupleTableSlot(slot); + + ereport(NOTICE, (errmsg("Total change: " INT64_FORMAT, delta))); + + return PointerGetDatum(NULL); + +} diff --git a/contrib/statement_trigger_row/statement_trigger_row.control b/contrib/statement_trigger_row/statement_trigger_row.control new file mode 100644 index 0000000..846ea2d --- /dev/null +++ b/contrib/statement_trigger_row/statement_trigger_row.control @@ -0,0 +1,5 @@ +# statement_trigger_row extension +comment = 'Statement trigger row' +default_version = '1.0' +module_pathname = '$libdir/statement_trigger_row' +relocatable = true