/* $Id: tuplock.c.src 137 2017-10-08 16:28:45Z coelho $ * * tuplock: PostgreSQL extension to lock tuples * version: 1.2.2 * url: http://www.coelho.net/tuplock * * (c) 2004-2017 Fabien Coelho < fabien at coelho dot net > * * This is free software, both inexpensive and provided with sources. * The GNU General Public Licence v3 applies, for details, see: * http://www.gnu.org/copyleft/gpl.html * The summary is: you get as much as you paid for, and I am not * responsible for anything. Beware that you may lose your data, your * business or your friends because of this software. * * A conservative trigger for locking tuples. See README for usage. */ #include "postgres.h" #include "catalog/pg_type.h" // for BOOLOID #include "executor/spi.h" #include "commands/trigger.h" #include "funcapi.h" // for RelationNameGetTupleDesc #include "access/htup_details.h" // for heap_getattr // check server compatibility PG_MODULE_MAGIC; // declare trigger extern Datum tuplock(PG_FUNCTION_ARGS); PG_FUNCTION_INFO_V1(tuplock); /* this trigger expects a BOOLEAN attribute name as an argument. * should be called as BEFORE UPDATE OR DELETE */ Datum tuplock(PG_FUNCTION_ARGS) { TriggerData * context; Trigger * trig; char * attname; bool isnull, locked; TupleDesc tupdes; int i, attnum; // sanity checks if (!CALLED_AS_TRIGGER(fcinfo)) elog(ERROR, "lock_tuple_trig: not called by trigger manager!"); context = (TriggerData *) fcinfo->context; trig = context->tg_trigger; // this special case is okay and blocked if (!TRIGGER_FIRED_FOR_ROW(context->tg_event) && TRIGGER_FIRED_BY_TRUNCATE(context->tg_event)) elog(ERROR, "trigger \"%s\" on \"%s\": " "items may be locked, cannot TRUNCATE", trig->tgname, SPI_getrelname(context->tg_relation)); if (!TRIGGER_FIRED_FOR_ROW(context->tg_event)) elog(ERROR, "trigger \"%s\" on \"%s\": must be called FOR EACH ROW", trig->tgname, SPI_getrelname(context->tg_relation)); if (!TRIGGER_FIRED_BY_UPDATE(context->tg_event) && !TRIGGER_FIRED_BY_DELETE(context->tg_event)) elog(ERROR, "trigger \"%s\" on \"%s\":" " must be called for UPDATE or DELETE", trig->tgname, SPI_getrelname(context->tg_relation)); // should I check that is is fired as BEFORE? no, not an issue. if (trig->tgnargs!=1) elog(ERROR, "trigger \"%s\" on \"%s\":" " expecting attribute name as only argument", trig->tgname, SPI_getrelname(context->tg_relation)); // look for relation.attname boolean attribute. // can't use GetAttributeByName here, not the right kind of HeapTuple. // moreover, we want to check that the type is really bool. attname = trig->tgargs[0]; // tupdes = context->tg_relation->rd_att; tupdes = RelationNameGetTupleDesc(SPI_getrelname(context->tg_relation)); attnum = 0; // 0 is not found for (i=0; inatts; i++) { Form_pg_attribute a = tupdes->attrs[i]; if (strcasecmp(NameStr(a->attname), attname) == 0) { if (a->atttypid!=BOOLOID) elog(ERROR, "trigger \"%s\" on \"%s\": attribute \"%s\" is not BOOL", trig->tgname, SPI_getrelname(context->tg_relation), attname); // found and type is ok attnum = a->attnum; break; } } if (attnum == 0) elog(ERROR, "trigger \"%s\" on \"%s\": attribute \"%s\" not found", trig->tgname, SPI_getrelname(context->tg_relation), attname); locked = DatumGetBool(heap_getattr(context->tg_trigtuple, attnum, tupdes, &isnull)); // raise exception if NULL or locked. if (isnull || locked) elog(ERROR, "trigger \"%s\" on \"%s\": item locked by attribute \"%s\"", trig->tgname, SPI_getrelname(context->tg_relation), attname); // else return tuple to executor for update or delete. return PointerGetDatum(TRIGGER_FIRED_BY_UPDATE(context->tg_event) ? context->tg_newtuple : context->tg_trigtuple); }