How do you ensure that a table is only updated when a new modified_at is greater than the current modified_at in PostgreSQL? - sql

I have a situation where I want to deny an update to a table if the current modified_at column is more recent than the one specified in the update. I attempted to do this with a CHECK constraint, but it has no effect.
Here is an example:
CREATE TABLE test (
id SERIAL PRIMARY KEY,
value INTEGER NOT NULL DEFAULT 0,
modified_at timestamptz DEFAULT NOW()
);
ALTER TABLE test ADD CHECK (modified_at >= modified_at);
INSERT INTO test (id, value) VALUES (1, 1);
INSERT 0 1
SELECT * FROM test;
id | value | modified_at
----+-------+-------------------------------
1 | 1 | 2013-05-30 14:34:37.234456-07
UPDATE test
SET value = 2, modified_at = NOW() - INTERVAL '1 day'
WHERE id = 1;
UPDATE 1
SELECT * FROM test;
id | value | modified_at
----+-------+-------------------------------
1 | 2 | 2013-05-29 14:35:41.337543-07
This doesn't appear to work as expected. Intuitively I could see this being a problem. How does the planner know that the left hand side should be the new value and the right hand side should be the old?
Knowing this doesn't work, how should this constraint be enforced?

You will have to check old modified_date against new modified_date of the updated row, and you can do this using triggers.
Set the trigger to run on each row before update and create a function that deals with it, choosing if you want to keep the old modified_date or if you don't want to perform any update at all.
The trigger can be done like this:
CREATE TRIGGER trigger_test
BEFORE INSERT OR UPDATE
ON test
FOR EACH ROW
EXECUTE PROCEDURE fn_trigger_test();
And the function like this:
CREATE OR REPLACE FUNCTION fn_trigger_test()
RETURNS trigger AS
$BODY$
DECLARE
BEGIN
IF (TG_OP = 'UPDATE') THEN
IF NEW.modified_at<OLD.modified_at THEN
RAISE EXCEPTION 'Date_modified older than previous';
/* or to keep the old modified date:
NEW.modifed_at=OLD.modified_at;
RETURN NEW; */
ELSE
RETURN NEW;
END IF;
END IF;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;

As far as CHECK constraints are concerned, there's no such thing as new values and old values.
PostgreSQL triggers have access to new data. Another fairly common approach is to revoke permissions on the tables, and require all access to take place through stored procedures. Stored procedures can have access to new data, passed through parameters, and stored procs can check values in tables, update additional tables for auditing, etc. See CREATE FUNCTION and plpgsql.

Related

Oracle trigger to prevent inserting the new row upon a condition

I've found few questions addressing the same question but without a better solution.
I need to create an Oracle trigger which will prevent new inserts upon a condition, but silently (without raising an error).
Ex : I need to stop inserting rows with bar='FOO' only. (I can't edit the constraints of the table, can't access the procedure which really does the insertion etc so the trigger is the only option)
Solutions so far confirms that it isn't possible. One promising suggestion was to create an intermediate table, insert key values to that when bar='FOO' and then delete those records from original table once insertion is done, which is not correct I guess.
Any answer will be highly appreciated.
Apparently, it is not possible to use a trigger to stop inserts without raising an exception.
However, if you have access to the schema (and asking about a trigger this is probably ok), you could think about replacing the table with a view and an instead of trigger.
As a minimal mock up for your current table. myrole is just a stand in for the privileges granted on the table:
CREATE ROLE myrole;
CREATE TABLE mytable (
bar VARCHAR2(30)
);
GRANT ALL ON mytable TO myrole;
Now you rename the table and make sure nobody can directly access it anymore, and replace it with a view. This view can be protected by a instead of trigger:
REVOKE ALL ON mytable FROM myrole;
RENAME mytable TO myrealtable;
CREATE OR REPLACE VIEW mytable AS SELECT * FROM myrealtable;
GRANT ALL ON mytable TO myrole;
CREATE OR REPLACE TRIGGER myioftrigger
INSTEAD OF INSERT ON mytable
FOR EACH ROW
BEGIN
IF :new.bar = 'FOO' THEN
NULL;
ELSE
INSERT INTO myrealtable(bar) VALUES (:new.bar);
END IF;
END;
/
So, if somebody is inserting a normal row into the fake view, the data gets inserted into your real table:
INSERT INTO mytable(bar) VALUES('OK');
1 row inserted.
SELECT * FROM mytable;
OK
But if somebody is inserting the magic value 'FOO', the trigger silently swallows it and nothing gets changed in the real table:
INSERT INTO mytable(bar) VALUES('FOO');
1 row inserted.
SELECT * FROM mytable;
OK
Caution: If you want to protect your table from UPDATEs as well, you'd have to add a second trigger for the updates.
One way would be to hide the row. From 12c this is reasonably easy:
create table demo
( id integer primary key
, bar varchar2(10) );
-- This adds a hidden column and registers the table for in-database archiving:
alter table demo row archival;
-- Set the hidden column to '1' when BAR='FOO', else '0':
create or replace trigger demo_hide_foo_trg
before insert or update on demo
for each row
begin
if :new.bar = 'FOO' then
:new.ora_archive_state := '1';
else
:new.ora_archive_state := '0';
end if;
end demo_hide_foo_trg;
/
-- Enable in-database archiving for the session
-- (probably you could set this in a log-on trigger):
alter session set row archival visibility = active;
insert into demo (id, bar) values (1, 'ABC');
insert into demo (id, bar) values (2, 'FOO');
insert into demo (id, bar) values (3, 'XYZ');
commit;
select * from demo;
ID BAR
-------- --------
1 ABC
3 XYZ
-- If you want to see all rows (e.g. to delete hidden rows):
alter session set row archival visibility = all;
In earlier versions of Oracle, you could achieve the same thing using a security policy.
Another way might be to add a 'required' flag which defaults to 'Y' and set it to to 'N' in a trigger when bar = 'FOO', and (assuming you can't change the application to use a view etc) have a second trigger delete all such rows (or perhaps better, move them to an archive table).
create table demo
( id integer primary key
, bar varchar2(10) );
alter table demo add required_yn varchar2(1) default on null 'Y';
create or replace trigger demo_set_not_required_trg
before insert or update on demo
for each row
begin
if :new.bar = 'FOO' then
:new.required_yn := 'N';
end if;
end demo_hide_foo_trg;
/
create or replace trigger demo_delete_not_required_trg
after insert or update on demo
begin
delete demo where required_yn = 'N';
end demo_delete_not_required_trg;
/

Trigger Function → Selecting a unique row using a values in a none unique column

I got a sticky situation here whereby I setup a trigger to update a table (Self-updating function). What I got here is that the function is able to identify that there is an update operation however, it cannot located the row to update as there are no unique value in the column.
TRIG_NS_ABS4_To_Area_func (Trigger Func):
BEGIN
IF (TG_OP = 'UPDATE') AND (OLD."ABS4" <> NEW."ABS4")
THEN UPDATE systems."NS_HandoverReportInput_tbl" SET ("Area") = ((SELECT "NS_AREA" FROM systems."NS_ABS4Area Match_tbl" WHERE "NS_ABS4" = NEW."ABS4"))
WHERE "NSItemNumber" = NEW."NSItemNumber";
END IF;
RETURN NEW;
END;
I was wondering whether does anyone have any idea to locate the row to update.
Please bear in mind that ONLY the "NSItemNumber" field is unique else the rest of the fields may have repeating values.
Script:
CREATE TABLE systems."NS_HandoverReportInput_tbl" (
"NSItemNumber" SERIAL,
"ABS4" TEXT,
"Area" TEXT,
CONSTRAINT "PK_NS_HandoverReportInput_tbl" PRIMARY KEY("NSItemNumber"),
)
WITH (oids = false);
CREATE TRIGGER "NS_ABS4Area Match_tbl"
AFTER INSERT OR UPDATE
ON systems."NS_HandoverReportInput_tbl" FOR EACH ROW
EXECUTE PROCEDURE systems."TRIG_NS_ABS4_To_Area_func"();
NS_ABS4Area Match_tbl display info as listed:
NSItemNumber | ABS4 | Area
1001 | AAAA |Toilet
1002 | AABB |Central Area
1003 | AACC |Carpark
1004 | AAAA |Toilet
1005 | AABB |Central Area
I'll give you two solutions, a working one and a good one.
The working solution
Use a BEFORE INSERT OR UPDATE trigger, then you don't have to locate the row to update, because instead of changing the table you change the values before they are written to the table.
You could define your trigger like this:
CREATE OR REPLACE FUNCTION "TRIG_NS_ABS4_To_Area_func"() RETURNS trigger LANGUAGE plpgsql AS
$$BEGIN
-- this will fail if there is more than one "NS_AREA" per "NS_ABS4"
SELECT DISTINCT "NS_AREA" INTO STRICT NEW."NS_AREA"
FROM "NS_ABS4Area Match_tbl"
WHERE "NS_ABS4" = NEW."ABS4";
RETURN NEW;
END;$$;
CREATE TRIGGER "NS_ABS4Area Match_tbl"
BEFORE INSERT OR UPDATE
ON systems."NS_HandoverReportInput_tbl" FOR EACH ROW
EXECUTE PROCEDURE systems."TRIG_NS_ABS4_To_Area_func"();
The good solution
You avoid the whole mess by normalizing your database design.
That way no inconsistencies can ever happen, and you don't need a trigger.
CREATE TABLE area_description (
abs4 text PRIMARY KEY,
area text NOT NULL
);
COPY area_description FROM STDIN (FORMAT 'csv');
'AAAA', 'Toilet'
'AABB', 'Central Area'
'AACC', 'Carpark'
\.
CREATE TABLE ns_report_input (
ns_item_number serial PRIMARY KEY,
abs4 text REFERENCES area_description(abs4)
);
CREATE INDEX ns_report_input_fkex_ind ON ns_report_input(abs4);
You can define a view if you want something that looks like your original table.

Trigger to update a different table

Using Postgres 9.4, I have 2 tables streams and comment_replies. I am trying to do is update the streams.comments count each time a new comment_replies is inserted to keep track of the number of comments a particular stream has. I am not getting any errors but when I try to create a new comment it gets ignored.
This is how I am setting up my trigger. stream_id is a foreign key, so every stream_id will correspond to a streams.id which is the primary key of the streams table. I have been looking at this example: Postgres trigger function, but haven't been able to get it to work.
CREATE TABLE comment_replies (
id serial NOT NULL PRIMARY KEY,
created_on timestamp without time zone,
comments text,
profile_id integer,
stream_id integer
);
The trigger function:
CREATE OR REPLACE FUNCTION "Comment_Updates"()
RETURNS trigger AS
$BODY$BEGIN
update streams set streams.comments=streams.comments+1
where streams.id=comment_replies_streamid;
END$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
And the trigger:
CREATE TRIGGER comment_add
BEFORE INSERT OR UPDATE
ON comment_replies
FOR EACH ROW
EXECUTE PROCEDURE "Comment_Updates"();
How can I do this?
There are multiple errors. Try instead:
CREATE OR REPLACE FUNCTION comment_update()
RETURNS trigger AS
$func$
BEGIN
UPDATE streams s
SET streams.comments = s.comments + 1
-- SET comments = COALESCE(s.comments, 0) + 1 -- if the column can be NULL
WHERE s.id = NEW.streamid;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER comment_add
BEFORE INSERT OR UPDATE ON comment_replies -- on UPDATE, too? Really?
FOR EACH ROW EXECUTE PROCEDURE comment_update();
You need to consider DELETE as well if that is possible. Also if UPDATE can change stream_id. But why increase the count for every UPDATE? This looks like another error to me.
It's a syntax error to table-qualify the target column in the SET clause of UPDATE.
You need to return NEW in a BEFORE trigger unless you want to cancel the INSERT / UPDATE.
Or you make it an AFTER trigger, which would work for this, too.
You need to reference NEW for the stream_id of the current row (which is automatically visible inside the trigger function.
If streams.comments can be NULL, use COALESCE.
And rather use unquoted, legal, lower-case identifiers.

Writing an SQL trigger to compare old and new values

I am trying to write a SQL trigger that compares the old and new values. If the two values are different then I need to display an error saying that you can't update the names. The exact definition of my trigger is
write a trigger function named disallow_team_name_update that compares
the OLD and NEW records team fields. If they are different raise an
exception that states that changing the team name is not allowed.
Then, attach this trigger to the table with the name tr disallow team
name update and specify that it fires before any potential update of
the team field in the table.
The table that I am using for this problem is:
Table "table.group_standings"
Column | Type | Modifiers
--------+-----------------------+-----------
team | character varying(25) | not null
wins | smallint | not null
losses | smallint | not null
draws | smallint | not null
points | smallint| not null
Indexes:
"group_standings_pkey" PRIMARY KEY, btree (team)
Check constraints:
"group_standings_draws_check" CHECK (draws >= 0)
"group_standings_losses_check" CHECK (losses >= 0)
"group_standings_points_check" CHECK (points >= 0)
"group_standings_wins_check" CHECK (wins >= 0)
Here is my code:
CREATE OR REPLACE FUNCTION disallow_team_name_update() RETURNS trigger AS $$
BEGIN
if(NEW.team <> OLD.team)
/*tell the user to not change team names*/
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER tr_disallow_team_name_update BEFORE INSERT OR UPDATE OF team ON group_standings
FOR EACH ROW EXECUTE PROCEDURE disallow_team_name_update();
PostgreSQL can use raise exception to, um, raise exceptions.
CREATE OR REPLACE FUNCTION disallow_team_name_update()
RETURNS trigger AS
$$
BEGIN
if(NEW.team <> OLD.team) then
raise exception 'Invalid update.'
using hint = 'Changes to team name are not allowed.';
end if;
END
$$
LANGUAGE plpgsql;
You surely don't want to disallow changes to the team name on insert. (PostgreSQL won't let you do it anyway.)
CREATE TRIGGER tr_disallow_team_name_update
BEFORE UPDATE OF team ON group_standings
FOR EACH ROW EXECUTE PROCEDURE disallow_team_name_update();

PLPGSQL Cascading Triggers?

I am trying to create a trigger, so that when ever I add a new record it adds another record in the same table. The session field will only take values between 1 and 4. So when I add a 1 in session I want it to add another record but with session 3 blocked. But the problem is that it leads to cascading triggers and it inserts itself again and again because the trigger is triggered when inserted.
I have for example a simple table:
CREATE TABLE example
(
id SERIAL PRIMARY KEY
,name VARCHAR(100) NOT NULL
,session INTEGER
,status VARCHAR(100)
);
My trigger function is:
CREATE OR REPLACE FUNCTION add_block() RETURNS TRIGGER AS $$
BEGIN
INSERT INTO example VALUES (NEW.id + 1, NEW.name, NEW.session+2, 'blocked');
RETURN NULL;
END;
$$ LANGUAGE 'plpgsql';
Trigger is:
CREATE TRIGGER add_block
AFTER INSERT OR UPDATE
ON example
FOR EACH ROW
EXECUTE PROCEDURE add_block();
I get error:
SQL statement "INSERT INTO example VALUES ( $1 +1, $2 , $3 + 2, $4)"
PL/pgSQL function "add_block" line 37 at SQL statement
This error repeats itself so many times that I can't see the top.
How would I solve this?
EDIT:
CREATE TABLE block_rules
(
id SERIAL PRIMARY KEY
,session INTEGER
,block_session INTEGER
);
This table holds the block rules. So if a new record is inserted into the EXAMPLE table with session 1 then it blocks session 3 accordingly by inserting a new record with blocked status in the same (EXAMPLE) table above (not block_rules). Same for session 2 but it blocks session 4.
The block_rules table holds the rules (or pattern) to block a session by. It holds
id | session | block_session
------------------------------
1 | 1 | 3
2 | 2 | 4
3 | 3 | 2
How would I put that in the WHEN statement of the trigger going with Erwin Branstetter's answer below?
Thanks
New answer to edited question
This trigger function adds blocked sessions according to the information in table block_rules.
I assume that the tables are linked by id - information is missing in the question.
I now assume that the block rules are general rules for all sessions alike and link by session. The trigger is only called for non-blocked sessions and inserts a matching blocked session.
Trigger function:
CREATE OR REPLACE FUNCTION add_block()
RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO example (name, session, status)
VALUES (NEW.name
,(SELECT block_session
FROM block_rules
WHERE session = NEW.session)
,'blocked');
RETURN NULL;
END;
$BODY$ LANGUAGE plpgsql;
Trigger:
CREATE TRIGGER add_block
AFTER INSERT -- OR UPDATE
ON example
FOR EACH ROW
WHEN (NEW.status IS DISTINCT FROM 'blocked')
EXECUTE PROCEDURE add_block();
Answer to original question
There is still room for improvement. Consider this setup:
CREATE OR REPLACE FUNCTION add_block()
RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO example (name, session, status)
VALUES (NEW.name, NEW.session + 2, 'blocked');
RETURN NULL;
END;
$BODY$ LANGUAGE plpgsql;
CREATE TRIGGER add_block
AFTER INSERT -- OR UPDATE
ON example
FOR EACH ROW
WHEN (NEW.session < 3)
-- WHEN (status IS DISTINCT FROM 'blocked') -- alternative guess at filter
EXECUTE PROCEDURE add_block();
Major points:
For PostgreSQL 9.0 or later you can use a WHEN condition in the trigger definition. This would be most efficient. For older versions you use the same condition inside the trigger function.
There is no need to add a column, if you can define criteria to discern auto-inserted rows. You did not tell, so I assume that only auto-inserted rows have session > 2 in my example. I added an alternative WHEN condition for status = 'blocked' as comment.
You should always provide a column list for INSERTs. If you don't, later changes to the table may have unexpected side effects!
Do not insert NEW.id + 1 in the trigger manually. This won't increment the sequence and the next INSERT will fail with a duplicate key violation.
id is a serial column, so don't do anything. The default nextval() from the sequence is inserted automatically.
Your description only mentions INSERT, yet you have a trigger AFTER INSERT OR UPDATE. I cut out the UPDATE part.
The keyword plpgsql doesn't have to be quoted.
OK so can't you just add another column, something like this:
ALTER TABLE example ADD COLUMN trig INTEGER DEFAULT 0;
CREATE OR REPLACE FUNCTION add_block() RETURNS TRIGGER AS $$
BEGIN
IF NEW.trig = 0 THEN
INSERT INTO example VALUES (NEXTVAL('example_id_seq'::regclass), NEW.name, NEW.session+2, 'blocked', 1);
END IF;
RETURN NULL;
END;
$$ LANGUAGE 'plpgsql';
it's not great, but it works :-)
CREATE OR REPLACE FUNCTION add_block() RETURNS TRIGGER AS $$
BEGIN
SET SESSION session_replication_role = replica;
INSERT INTO example VALUES (NEXTVAL('example_id_seq'::regclass), NEW.name, NEW.session+2, 'blocked');
SET SESSION session_replication_role = origin;
RETURN NULL;
END;
$$ LANGUAGE 'plpgsql';