trigger insert only if tupel satisfies condition - sql

I want to create a trigger which checks before insert if the tupel which is supposed to be inserted holds a specific condition (which also depends on another table).
For example:
create trigger or replace check_tupel
before insert on A
for each row
execute
if exists (select x,y from B where B.x = A.x and B.y = A.y)
Oh I am using postgreSQL 13.
EDIT: Yes I know that I can do this without a trigger, but I am asking for a solution with a trigger for a reason.
I hope there is a way to do this... My other idea was to create a UDF which gets called before insert but I do not know how to check the condition in this UDF and only insert if the function returns true.

If you simply wanna automatically validate a record before inserting it on table A based on table B using a User Defined Function, you do not need a trigger at all. Consider adding a simple CHECK CONSTRAINT:
CREATE TABLE a (
x int,
y int,
CONSTRAINT exists_in_b CHECK (NOT myfunc(x,y))
);
Demo: db<>fiddle
CREATE TABLE b (x int,
y int);
INSERT INTO b VALUES (42,42);
CREATE OR REPLACE FUNCTION myfunc(x int, y int)
RETURNS BOOLEAN AS $BODY$
SELECT EXISTS (SELECT 1 FROM b WHERE b.y =$1 AND b.x=$2 )
$BODY$
LANGUAGE sql;
CREATE TABLE a (
x int,
y int,
CONSTRAINT exists_in_b CHECK (NOT myfunc(x,y)) -- here the magic happens
);
Now, if we try to insert a value that our function does not validate, it raises an exception:
INSERT INTO a VALUES (42,42);
ERROR: new row for relation "a" violates check constraint "exists_in_b"
DETAIL: Failing row contains (42, 42).
SQL state: 23514
EDIT (See comments): Solution using a trigger
CREATE OR REPLACE FUNCTION myfunc()
RETURNS TRIGGER AS $BODY$
BEGIN
IF EXISTS (SELECT 1 FROM b WHERE b.y =NEW.y AND b.x=NEW.x ) THEN
RAISE EXCEPTION 'tuple already exists in "b": % %', NEW.x,NEW.y;
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER check_tupel
BEFORE INSERT OR UPDATE ON a
FOR EACH ROW EXECUTE PROCEDURE myfunc();
Demo: db<>fiddle

So you need a trigger solution, sounds like a homework problem. The question then becomes do you want to:
abort the entire operation
slightly squash the row but continue the remainder of the operation.
The following does the second: (See demo)
create or replace
function check_b_has_a()
returns trigger
language plpgsql
as $$
begin
if exists
( select null
from b
where (b.x,b.y) =
(new.x, new.y)
)
then
return null;
else
return new;
end if;
end;
$$;
create trigger a_bir
before insert
on a
for each row
execute function check_b_has_a();

Related

Return a table when a trigger function is called

I need some_fun() to be executed before a trigger. It will return a table.
I ran this:
INSERT INTO SomeTable(some_bool) VALUES (true);
I expected this:
returnColHeader
------------------
12
23
23
(3 row)
But I got this:
ERROR: query has no destination for result data
HINT: If you want to discard the results of a SELECT, use PERFORM instead.
I've read documentation on TRIGGERS https://www.postgresql.org/docs/current/plpgsql-trigger.html
and also RETURNING CLAUSE https://www.postgresql.org/docs/9.5/dml-returning.html
and a few other postgres related readings but I'm still unable to solve my problem.
DROP TRIGGER IF EXISTS run_some_fun on SomeTable CASCADE;
CREATE TRIGGER run_some_fun
BEFORE INSERT ON SomeTable
FOR EACH ROW WHEN (NEW.some_bool = TRUE)
EXECUTE FUNCTION run_some_fun();
CREATE OR REPLACE FUNCTION run_some_fun()
RETURNS TRIGGER AS $$
BEGIN
SELECT some_fun(NEW.eid); -- This is wrong and throws error
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION some_fun(eID INT)
RETURNS TABLE (returnColHeader INT) AS $$
BEGIN
RETURN QUERY
SELECT eid FROM Joins j1;
END;
$$ LANGUAGE plpgsql;
You cannot to return anything from after trigger. The trigger functions can returns value of composite type, but the returned value from after trigger is ignored. There is not any chance for what you want. And it looks little bit scary.

Using a procedure to update Column 2 in a Table A, by checking if column 1 value is present in a column in Table B [PostgreSQL]

I have two tables as follows:
Table A: Name, ID, value
Table B: ID, title
TableA.value is dependent on whether TableA.ID is present in TableB.ID.
I am trying to create a procedure and trigger so that whenever TableB is modified, the procedure is triggered to check if TableA.ID is in TableB.ID and sets TableA.value to 10.
I am using the following code and getting an error:
CREATE PROCEDURE update1
LANGUAGE SQL
AS $$
UPDATE tablA as a
SET a.value = 10
WHERE a.ID EXISTS ( SELECT b.ID
FROM tableB as b
WHERE a.ID = b.ID)
$$;
I am an SQL noob, and this is the first time I am trying to use a procedure.
UPDATE
I was able to create a procedure which does the job when I manually run it using CALL
However, it does not have a RETURNS TRIGGER block within it. Adding that returns the error Procedure cannot return triggers.
If I create a trigger as follows
CREATE TRIGGER b_trigger
AFTER UPDATE OR INSERT ON b.ID
FOR EACH STATEMENT
EXECUTE PROCEDURE update1();
However, this returns the following
ERROR: function columbia_deli.manager_discount must return type trigger
SQL state: 42P17
Try this using plpgsql Language:
Trigger Function:
CREATE OR REPLACE FUNCTION update1()
RETURNS trigger AS
$BODY$
begin
update "TableA" set value= 10 where id =new.id;
return NEW;
end;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
Then create trigger on TableB for after update event
CREATE TRIGGER trig_tableb
AFTER UPDATE
ON "TableB"
FOR EACH ROW
EXECUTE PROCEDURE update1();

Trigger caused by an update statement inside another trigger

Suppose I have the following tables:
CREATE TABLE foo (
id_foo int NOT NULL,
import int NOT NULL,
CONSTRAINT foo_pk PRIMARY KEY(id_foo)
);
CREATE TABLE bar (
id_bar int NOT NULL,
id_foo int NOT NULL,
import int NOT NULL,
CONSTRAINT bar_pk PRIMARY KEY(id_bar)
);
ALTER TABLE bar ADD CONSTRAINT fk_bar FOREIGN KEY(id_foo) REFERENCES foo(id_foo);
So, the import column of the foo table must equal the sum of the import column in bar table.
CREATE OR REPLACE FUNCTION foo_restriction() RETURNS TRIGGER AS
$$
BEGIN
IF (NEW.import != (SELECT COALESCE(SUM(import),0) FROM bar WHERE id_foo = NEW.id_foo)) THEN
RAISE EXCEPTION 'import column in foo must be equal in bar table.';
END IF;
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
But I want to automatically sum the new import value in bar to the foo reference.
CREATE OR REPLACE FUNCTION bar_insert() RETURNS TRIGGER AS
$$
BEGIN
UPDATE foo SET import = import + NEW.import WHERE id_foo = NEW.id_foo;
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
CREATE TRIGGER TR_fooRestriction AFTER INSERT OR UPDATE ON foo FOR EACH ROW EXECUTE PROCEDURE foo_restriction();
CREATE TRIGGER TR_barInsert AFTER INSERT ON bar FOR EACH ROW EXECUTE PROCEDURE bar_insert();
Then the following statement should not work:
INSERT INTO foo(id_foo, import) VALUES(1,25); --FAIL. Works ok.
The following instruction insert a correct tuple for foo, with import = 0.
INSERT INTO foo(id_foo, import) VALUES(2,0); --OK.
But, when I'm trying to add new values in the bat table, the database show me 'import column in foo must be equal in bar table.':
INSERT INTO bar(id_bar, id_foo, import) VALUES
(1,2,100),(2,2,160),(3,2,40);
Whats happening here?, both triggers are executed after each new row, so the restriction in foo table should not be activated in the last statement.
How can I do to fix it?
Thanks!
An AFTER INSERT trigger is executed after insert statement is executed, not after each row is inserted.
So your trigger should work well in statements inserting a single row, but not with multirow inserts.
To test this behaviour run this script in psql:
drop table if exists test;
create table test (id int);
create or replace function test_trigger()
returns trigger language plpgsql as $$
begin
raise notice '%', (select count(*) from test);
return null;
end $$;
create trigger test_trigger
after insert on test
for each row execute procedure test_trigger();
insert into test values (1), (2), (3);
DROP TABLE
CREATE TABLE
CREATE FUNCTION
CREATE TRIGGER
NOTICE: 3
NOTICE: 3
NOTICE: 3
INSERT 0 3
For the documentation:
When a row-level AFTER trigger is fired, all data changes made by the outer command are already complete, and are visible to the invoked trigger function.
You could update foo with a sum of values from bar in bar_insert():
update foo
set import = (
select sum(import)
from bar where id_foo = new.id_foo)
where id_foo = new.id_foo;
return null;
However, the best solution is a view. If foo only aggregates import, drop the table and create the view instead:
create or replace view foo_view as
select id_foo, sum(import) as import
from bar
group by id_foo;
If foo contains some other data, remove import from foo and create the view:
create or replace view foo_view as
select foo.*, sum(bar.import) as import_total
from bar
join foo using(id_foo)
group by foo.id_foo;
You do not need any triggers, all is done automatically.

Postgresql insert trigger does not work

New row is added to table A and i need trigger which will automatically insert row in table B after row has been inserted in table A.
CREATE FUNCTION insertblocked (
)
RETURNS trigger AS
$body$
BEGIN
INSERT INTO tableB (blocked.id,blocked.number,blocked.date)
VALUES (new.id,new.prefix,now())
RETURN NEW;
END
$body$
LANGUAGE 'plpgsql'
VOLATILE
CALLED ON NULL INPUT
SECURITY INVOKER;
CREATE TRIGGER insertblocked
AFTER INSERT
ON public.tableA FOR EACH ROW
EXECUTE PROCEDURE insertblocked();
Please help and advise, why is sql compiler returning and error
QUERY: INSERT INTO blocked (blocked.id,blocked.number,blocked.date) VALUES ( $1 , $2 ,now()) RETURN $3
You're missing a semi-column ; at the end of your insert statement.

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';