How to register trigger in PostgreSQL? - sql

I have trigger and procedure. I want to be sure that string saved to database will have no '-' characters.
After executing
UPDATE interesant SET interesant_nip = '555-555-5555'
I get error
value is too long for varchar(10).
This suggests that '-' characters were not removed before insert-update.
Why is my trigger not being executed?
CREATE FUNCTION normalize_nip() RETURNS TRIGGER AS $$
BEGIN
-- replace '' to NULL
IF (NEW.interesant_nip LIKE '') THEN
NEW.interesant_nip := NULL;
RETURN NEW;
END IF;
-- remove '-' from string
NEW.interesant_nip := REPLACE(NEW.interesant_nip, '-', '');
RETURN NEW;
END; $$ LANGUAGE plpgsql;"
CREATE TRIGGER interesant_nip_normalize BEFORE INSERT OR UPDATE ON public.interesant FOR EACH ROW EXECUTE PROCEDURE normalize_nip()

The updated or inserted value is always treated as type varchar(10) - before and after the trigger function. So, you cannot handle the value because the input type does not fit the value even if the trigger function converts it into a valid one.
It works if you have an unbounded text type. There you can see that the trigger is executed:
demo:db<>fiddle
So, the only chance to handle this, is, to normalize it before inserting or updating:
demo:db<>fiddle
INSERT INTO interesant VALUES (normalize_nip('555-555-5555'));
UPDATE interesant SET interesant_nip = normalize_nip('555-555-5555')

Related

loop stuck on Trigger postgresql

Im trying to create a trigger to controle parameters aren't NULL, but when i try to INSERT for example:
INSERT INTO puntuacions(puntuacio, contingut, data) VALUES (6,NULL,CURRENT_DATE);
Console start on a big LOOP trying to make INSERT inside function. Im not sure if i must do INSERT on function or at end of trigger.
BTW on avg select im trying to set an AVG value from a SELECT to insert on table.
I have this code:
FUNCTION:
create or replace function p_controla() returns trigger as $controla_puntuacions$
begin
IF new.puntuacio IS NULL OR new.contingut IS NULL OR new.data IS NULL
THEN
RAISE 'SIUSPLAU OMPLE TOTES LES DADES DE LA TAULA';
ELSE
INSERT INTO puntuacions(puntuacio, contingut, data) VALUES (new.puntuacio,new.contingut,new.data);
UPDATE continguts SET puntuacio = (SELECT AVG(puntuacio) FROM puntuacions WHERE contingut = new.contingut) WHERE idcontingut = new.contingut;
RAISE ' OK! ';
END IF;
END;
$controla_puntuacions$ language plpgsql;
TRIGGER:
create trigger controla_puntuacions
before insert on puntuacions
for each row execute procedure p_controla();
There are four problems with this trigger:
You expect that INSERT INTO puntuacions inserts a record which is handled by a trigger. A trigger does not work this way. INSERT INTO puntuacions in the trigger function instructs the database to insert another row. It will also fire this trigger again and will cause an infinite recursion. The row being inserted is in new. Values in new will be inserted to this table after the trigger returns new.
UPDATE continguts based on data in puntuacions is not appropriate in the before insert trigger, because the row is not inserted yet.
RAISE ' OK! ' raises an exception. So, the insert won't complete correctly even if all values are not null.
The trigger lacks return. When you fix 3, this will lead to a runtime error.
One issue is with the syntax for each row execute procedure. This syntax still works, but it is old-fashioned and now it is deprecated. The keyword FUNCTION should be used here instead of PROCEDURE.
For updating continguts you can create an after insert trigger.
If you want to insert a row after the trigger completes, it has to return new.
If you want just to display a message "OK", you can use RAISE NOTICE.
Potential solution:
The function for a before insert trigger:
create or replace function p_controla() returns trigger as $controla_puntuacions$
begin
IF new.puntuacio IS NULL OR new.contingut IS NULL OR new.data IS NULL
THEN
RAISE 'SIUSPLAU OMPLE TOTES LES DADES DE LA TAULA';
ELSE
RAISE NOTICE ' OK! ';
END IF;
RETURN new;
END;
$controla_puntuacions$ language plpgsql;
After insert trigger function:
create or replace function puntuacions_after_ins() returns trigger as $puntuacions_after_ins$
begin
UPDATE continguts SET puntuacio = (SELECT AVG(puntuacio) FROM puntuacions WHERE contingut = new.contingut) WHERE idcontingut = new.contingut;
RETURN new;
END;
$puntuacions_after_ins$ language plpgsql;
After insert trigger:
create trigger puntuacions_after_ins
after insert on puntuacions
for each row execute function puntuacions_after_ins();
The return value in the after insert function is ignored, but anyway this function has to return something.

Function returns bad value inside of a trigger

I have two functions that return the good value. But when I call those functions inside of a trigger they always returns 0 instead of the good value.
The return type of those functions is real. The direct and dramatic consequence is that the trigger inserts wrong values in tables when it is called.
The function:
create or replace function get_remaining_hour(id_user_v integer,id_absence_v_type integer,id_year_v integer) returns real as
$BODY$
BEGIN
return (select sum(number_hour)
from remaining_absence_day
where id_user= $1
and id_absence_type=$2
and id_year=$3 );
END;
$BODY$
LANGUAGE 'plpgsql' ;
The trigger function (modified for testing!):
create OR REPLACE function update_absence() returns TRIGGER AS
$BODY$
DECLARE
old_number_hour real;
BEGIN
old_number_hour:=get_remaining_hour(3,2,8);
insert into debugging(col,val) values('old_number_hour', old_number_hour);
return null;
END;
$BODY$
LANGUAGE 'plpgsql' ;
The trigger definition:
drop trigger if exists update_absence on absence;
CREATE TRIGGER update_absence
after update of type,duration_hour,duration_day on absence
for each ROW
execute procedure update_absence();
The presented code should work.
It is particularly odd that you see 0 as result. If no matching row is found in remaining_absence_day, you would see NULL, not 0. But if you call the function with the same parameters in the same environment you should see the same result to begin with.
The remaining possible explanation I can think of: confusion with the schema search path. Like: you have a second instance of the function get_remaining_hour() or the table remaining_absence_day in a different schema. And you call the function with a different setting for search_path.
Did you run your comparison in the same session?
How does the search_path influence identifier resolution and the "current schema"
Or, since you work with an AFTER trigger: there might be other triggers on table absence that modify the table remaining_absence_day, which are fired before your trigger.
All other modifications I made are of cosmetic nature or minor simplifications.
CREATE OR REPLACE FUNCTION get_remaining_hour(id_user_v int
, id_absence_v_type int
, id_year_v int)
RETURNS real AS
$func$
BEGIN
RETURN (
SELECT sum(number_hour)
FROM remaining_absence_day -- referencing the right table? see search_path
WHERE id_user = $1
AND id_absence_type = $2
AND id_year = $3
);
END
$func$ LANGUAGE plpgsql STABLE; -- don't quote the language name
CREATE OR REPLACE FUNCTION update_absence()
RETURNS TRIGGER AS
$func$
BEGIN
INSERT INTO debugging(col, val)
VALUES('old_number_hour', get_remaining_hour(3,2,8)); -- hard coded only for testing?
RETURN null; -- only good for AFTER trigger
END
$func$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS update_absence ON absence;
CREATE TRIGGER update_absence
AFTER UPDATE OF type, duration_hour, duration_day ON absence
FOR EACH ROW EXECUTE PROCEDURE update_absence();

Using the NEW variable in trigger functions

PostgreSQL 9.4
I'm working on the following trigger function:
CREATE OR REPLACE FUNCTION check_inserted_row() RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
--Here I need to transform the NEW trigger variable
--to an array of values or something else that can be iterated over
--and check each value in it
--How can I do that?
RETURN NEW;
END IF;
END $$
LANGUAGE plpgsql;
The issue is I don't know for sure what columns will be contained in NEW as that it might happen that some columns will be added later. To avoid rewriting the trigger any time it happens I'd like to iterate over all columns in NEW and perform required checking.
Is it possible?

how to get current row value in plpgsql function

I need to create plpgsql methods which use current row value without passing in parameter in update command.
I tried
create temp table test ( test text, result text ) on commit drop;
insert into test values ('1','');
CREATE OR REPLACE FUNCTION public.gettest() RETURNS text AS $$
DECLARE
comp text := NULL;
BEGIN
EXECUTE 'SELECT ''Current row is '' ||test.test' INTO comp;
RETURN comp;
END; $$ LANGUAGE plpgsql STRICT STABLE;
update test set result = 'Result: ' || gettest();
but got exception
ERROR: missing FROM-clause entry for table "test"
LINE 1: SELECT 'Current row is ' ||test.test
^
QUERY: SELECT 'Current row is ' ||test.test
CONTEXT: PL/pgSQL function gettest() line 6 at EXECUTE statement
********** Error **********
ERROR: missing FROM-clause entry for table "test"
SQL state: 42P01
Context: PL/pgSQL function gettest() line 6 at EXECUTE statement
How to fix ?
How to fix without passing vaue to plpgsql method parameter ?
There is no such thing as an "implicit current row". You have to pass the the function whatever it needs as a parameter. You can however pass a complete row if you want to:
create temp table test (val text, result text ) on commit drop;
insert into test values ('1','');
insert into test values ('2','');
CREATE OR REPLACE FUNCTION gettest(p_test test) RETURNS text AS $$
DECLARE
comp text := NULL;
BEGIN
comp := 'Current row is '||p_test.val;
RETURN comp;
END; $$ LANGUAGE plpgsql STRICT STABLE;
update test set result = 'Result: ' || gettest(test);
I had to rename the column test to something else, otherwise the call gettest(test) would refer to the column not the whole table (=row) and thus it didn't work.

SQL, update column in a specific row, instead of all rows

The task is to update the specific row in the column klienta_nr, which is located in the table klientu_ieteikumi. If a specific row is deleted in klienti table. This code updates the whole column not the specific row. What is the problem?
CREATE FUNCTION "funkc"() RETURNS "opaque" AS '
DECLARE
BEGIN
IF (TG_OP = ''DELETE'') THEN
UPDATE klientu_ieteikumi SET klienta_nr = NULL ;
END IF;
RETURN NEW;
END;
' LANGUAGE 'plpgsql';
CREATE TRIGGER "triger"
AFTER DELETE ON "klienti"
FOR EACH ROW EXECUTE PROCEDURE funkc();
This one did what I wanted, thanks anyway everyone :)
CREATE FUNCTION "funkcija1"() RETURNS TRIGGER AS $$
BEGIN
UPDATE klientu_ieteikumi SET klienta_nr = NULL
FROM klienti WHERE old.klienta_nr = klientu_ieteikumi.klienta_nr;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "trigeris"
AFTER DELETE ON "klienti"
FOR EACH ROW EXECUTE PROCEDURE funkcija1();
you need a WHERE clause to restrict the number of rows affected by the statement.
edit:
UPDATE klientu_ieteikumi
SET klienta_nr = NULL
WHERE klienta_ieteikumi.klienta_nr = klienti.klienta_nr
You presented "solution" cannot work and is wrong in a sneaky way.
CREATE FUNCTION funkcija1()
RETURNS TRIGGER AS
$func$
BEGIN
CREATE FUNCTION funkcija1()
RETURNS TRIGGER AS
$func$
BEGIN
UPDATE klientu_ieteikumi
SET klienta_nr = NULL
FROM klienti -- !!
WHERE klientu_ieteikumi.klienta_nr = OLD.klienta_nr;
RETURN NEW;
RETURN NULL;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER trigeris
AFTER DELETE ON klienti
FOR EACH ROW EXECUTE PROCEDURE funkcija1();
There is no NEW in an AFTER trigger. This wouldn't work at all and raise an exception immediately.
The unbound (and utterly pointless) table klienti in the FROM clause leads to a CROSS JOIN. I.e., instead of just one time, the UPDATE is executed as many times as there are rows in klienti. This would be a major drag on performance, and you might never find out, since there is no error message. Just a lot of wasted cycles and table bloat on your server.