Can I iterate over NEW in a PostgreSQL trigger function? - postgresql

I've written a trigger function hoping to iterate over NEW and check all its values.
CREATE OR REPLACE FUNCTION fix_nulls() RETURNS TRIGGER AS $_$
BEGIN
FOR val IN NEW
LOOP
IF val = '{x:Null}'
val := '';
ENDIF;
ENDLOOP;
RETURN NEW;
END $_$ LANGUAGE 'plpgsql';
CREATE TRIGGER prevent_nulls_siteinfo
BEFORE UPDATE OR INSERT ON siteinfo
FOR EACH ROW
EXECUTE PROCEDURE fix_nulls();
but I get a syntax error:
ERROR: syntax error at or near "NEW"
LINE 3: FOR val IN NEW
^
Is it possible to iterate over all values in NEW? I could easily write a bunch of if statements to check each column, but I'd prefer this function be general so I can use it for other tables in the future.

Static code for simple cases
For just a bunch of given columns I would just spell it out.
CREATE OR REPLACE FUNCTION fix_nulls()
RETURNS TRIGGER AS
$func$
BEGIN
IF NEW.val1 = '{x:Null}' THEN NEW.val1 := ''; END IF;
IF NEW.val2 = '{x:Null}' THEN NEW.val2 := ''; END IF;
IF NEW.va31 = '{x:Null}' THEN NEW.val3 := ''; END IF;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
Don't quote the language name plpgsql, it's an identifier.
And only fire the trigger when relevant:
CREATE TRIGGER prevent_nulls_siteinfo
BEFORE UPDATE OR INSERT ON siteinfo
FOR EACH ROW
WHEN ('{x:Null}' IN (NEW.val1, NEW.val2, NEW.val3))
EXECUTE PROCEDURE fix_nulls();
Dynamic code
If there are many columns or (better reason) columns (names) change a lot, a dynamic approach might be warranted. However, while a plpgsql FOR loop can iterate over rows in a set (table) or over elements in an array, it can not do the same with columns in a row.
Why?
PL/pgSQL: General Way to Update N Columns in Trigger?

Related

How to update the tables NEW values after INSERT Trigger in PostgreSQL/PostGIS?

I try to automatize some calculations on tables in my database. I try to perform some UPDATE on rows that are newly inserted in a table, but I newer used NEW or OLD statements before. I tried writing the code that updates happen on new values by assigning NEW.[tablename], but it wont work. Isn't there any statement in the beginning of the trigger function to specify running the function only on new values, I cannot find useful information about this.
CREATE OR REPLACE FUNCTION cost_estimation()
RETURNS TRIGGER AS
$func$
DECLARE
a INTEGER := 3;
BEGIN
UPDATE NEW.cost_table
SET column4 = a;
UPDATE NEW.cost_table
SET column 5 = column4 - column2;
[...]
RETURN NEW;
END
$func$ language plpgsql
UPDATE:
Thank you for the answers so far.
My original code is written based on the update structure, and needs to be rewritten when omitting UPDATE. I should give a better example of my situation. Easy spoken: I have a table (T1) which will be filled with data from another table (T2).
After data is inserted in T1 from T2 I want to run calculations on the new values inside of T1.(The code includes PostGIS functionalities):
CREATE OR REPLACE FUNCTION cost_estimation()
RETURNS TRIGGER AS
$func$
BEGIN
NEW.column6 = column2 FROM external_table WHERE
St_Intersects(NEW.geom, external_table.geom) LIMIT1;
NEW.column8 = CASE
WHEN st_intersects(NEW.geom, external_table2.geom) then 'intersects'
WHEN (NEW.column9 = 'K' and NEW.column10 <= 6) then 'somethingelse'
ELSE 'nothing'
END
FROM external_table2;
[...]
RETURN NEW;
END
$func$ language plpgsql
CREATE TRIGGER table_calculation_on_new
BEFORE INSERT OR UPDATE ON cost_estimation
FOR EACH ROW EXECUTE PROCEDURE road_coast_estimation();
After inserting values in my table no calculations will be performed.
UPDATE2: I checked my tables again and detected that another trigger was blocking the table operation. The code in the lower half is working fine now, thanks to #a_horse_with_no_name.
NEW and OLD aren't "statements", those are records that represent the modified rows from the DML statement that fired the trigger.
Assuming the trigger is defined on cost_table you can simply change the fields in the NEW record. No need to UPDATE anything:
CREATE OR REPLACE FUNCTION cost_estimation()
RETURNS TRIGGER AS
$func$
DECLARE
a INTEGER := 3;
BEGIN
new.column4 := a;
new.column5 := new.column4 - new.column2;
return new;
END;
$func$ language plpgsql
For this to work the trigger needs to be defined as a BEFORE trigger:
create trigger cost_table_trigger
BEFORE insert or update on cost_table
for each row execute procedure cost_estimation();

How to use json string element in a trigger function without declaring it as a variable

I'm learning about Postgresql - I don't have a specific application in mind, but the general idea is to update one column with json and not worry about the rest.
I have a table with a few columns (id, name, amount, json). I would like to update only the json column and have a trigger update the other columns from the json.
For example, if json was set to {"name" = "fred", "amount" = 100}, the trigger would populate the name and amount columns
so far, this works:
create table one(id serial primary key, name text, amount int, data jsonb);
CREATE OR REPLACE FUNCTION test()
RETURNS trigger AS
$$
DECLARE
json_name text = (data -> 'name') from one;
json_amount int = (data -> 'amount') from one;
BEGIN
IF json_name IS DISTINCT FROM OLD.name THEN
UPDATE one SET name = json_name;
END IF;
IF json_amount IS DISTINCT FROM OLD.amount THEN
UPDATE one SET amount = json_amount;
END IF;
RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';
CREATE TRIGGER test_trigger
AFTER INSERT OR UPDATE
OF data ON one
FOR EACH ROW
EXECUTE PROCEDURE test();
I'm trying to do this without declaring any variables. for example, I've tried these:
IF one.data->'name' IS DISTINCT FROM OLD.name THEN
or
IF one.data->'name'::text IS DISTINCT FROM OLD.name THEN
or
IF ((data->'name') from one) IS DISTINCT FROM OLD.name THEN
but none work.
Making such a trigger may be a horrible idea, and that would be nice to know, but I'm mainly interested figuring out this json stuff :)
I've made an example here:
https://dbfiddle.uk/?rdbms=postgres_11&fiddle=92f2e6dd3630c76178ca4cfe4dc30b10
Make it a before trigger and instead of an UPDATE manipulate new.
CREATE OR REPLACE FUNCTION test()
RETURNS trigger
AS
$$
BEGIN
new.name := new.data->'name';
new.amount := new.data->'amount';
RETURN new;
END;
$$
LANGUAGE plpgsql;
CREATE TRIGGER test_trigger
BEFORE INSERT
OR UPDATE
OF data
ON one
FOR EACH ROW
EXECUTE PROCEDURE test();
Although the answer provided by #sticky bit goes into the right direction, it doesn't handle type conversion well.
Here's a version that is better at dealing with empty JSON strings:
CREATE OR REPLACE FUNCTION test()
RETURNS trigger AS
$$
BEGIN
NEW.name := (NEW.data->>'name');
NEW.amount := round((NEW.data->>'amount')::numeric)::int;
RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';
CREATE TRIGGER test_trigger
BEFORE INSERT OR UPDATE
OF data ON one
FOR EACH ROW
EXECUTE PROCEDURE test();
https://dbfiddle.uk/?rdbms=postgres_11&fiddle=15a232d4092e23c7bc0425b849b31976
Please remember that executing multiple UPDATEs inside a trigger function after an original INSERT or UPDATE is going to kill performance.

Error in PostgreSQL trigger

i'm coding this trigger in postgreSQL
CREATE OR REPLACE FUNCTION fn_trg_viabilidad_fila()
RETURNS trigger AS
$BODY$
BEGIN
PERFORM S.*
FROM MontoMinimo M, SolicitudPresupuesto S, Cantidad C, Producto P
WHERE P.idProducto=C.idProducto
and C.idPresupuesto=S.idPresupuesto
and M.idMonto=S.idMonto;
IF (C.cantidad < P.canMinExp OR P.exportable = FALSE)
THEN
UPDATE SolicitudPresupuesto
SET viable = FALSE
WHERE idPresupuesto = OLD.idPresupuesto;
RETURN NEW;
END IF;
END
$BODY$
LANGUAGE plpgsql
CREATE TRIGGER trg_viabilidad_fila BEFORE INSERT
OR UPDATE ON SolicitudPresupuesto
FOR EACH ROW EXECUTE PROCEDURE
fn_trg_viabilidad_fila() ;
I can't solve this error..
An error has occurred: ERROR: missing FROM-clause entry for table "c"
LINE 1: SELECT C.cantidad < P.canminexp OR P.exportable = FALSE ^
QUERY: SELECT C.cantidad < P.canminexp OR P.exportable = FALSE
CONTEXT: PL/pgSQL function fn_trg_viabilidad_fila() line 9 at IF
I will be very grateful to any help. Sorry for my bad english
You can't access the columns of a query outside of the query (or the block where you use the query). You need to store the result of the select somewhere. Additionally you shouldn't run an UPDATE on the triggered table, you need to assign the value to the NEW record.
CREATE OR REPLACE FUNCTION fn_trg_viabilidad_fila()
RETURNS trigger AS
$BODY$
DECLARE
l_result boolean;
BEGIN
SELECT (c.cantidad < p.canMinExp OR p.exportable = FALSE)
INTO l_result
FROM MontoMinimo M
JOIN SolicitudPresupuesto s ON m.idMonto = s.idMonto
JOIN Cantidad c ON c.idPresupuesto = s.idPresupuesto
JOIN Producto p ON p.idProducto = c.idProducto;
IF l_result THEN
new.viable := false;
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
It would be possible to "inline" the query into the IF statement but this way it resembles the structure of your current code better. Also note that I replaced the old, outdated implicit joins by an explicit and more robust JOIN operator.
The assigment new.viable assumes that idpresupuesto is the PK in the table solicitudpresupuesto (because you used that in the WHERE clause of the UPDATE statement)

Endless loop in trigger function

This is a trigger that is called by either an insert, update or a delete on a table. It is guaranteed the calling table has all the columns impacted and a deletes table also exists.
CREATE OR REPLACE FUNCTION sample_trigger_func() RETURNS TRIGGER AS $$
DECLARE
operation_code char;
table_name varchar(50);
delete_table_name varchar(50);
old_id integer;
BEGIN
table_name = TG_TABLE_NAME;
delete_table_name = TG_TABLE_NAME || '_deletes';
SELECT SUBSTR(TG_OP, 1, 1)::CHAR INTO operation_code;
IF TG_OP = 'DELETE' THEN
OLD.mod_op = operation_code;
OLD.mod_date = now();
RAISE INFO 'OLD: %', (OLD).name;
EXECUTE format('INSERT INTO %s VALUES %s', delete_table_name, (OLD).*);
ELSE
EXECUTE format('UPDATE TABLE %s SET mod_op = %s AND mod_date = %s'
, TG_TABLE_NAME, operation_code, now());
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
The ELSE branch triggers an endless loop. There may be more problems.
How to fix it?
The ELSE branch can be radically simplified. But a couple more things are inefficient / inaccurate / dangerous:
CREATE OR REPLACE FUNCTION sample_trigger_func()
RETURNS TRIGGER AS
$func$
BEGIN
IF TG_OP = 'DELETE' THEN
RAISE INFO 'OLD: %', OLD.name;
EXECUTE format('INSERT INTO %I SELECT ($1).*', TG_TABLE_NAME || '_deletes')
USING OLD #= hstore('{mod_op, mod_datetime}'::text[]
, ARRAY[left(TG_OP, 1), now()::text]);
RETURN OLD;
ELSE -- insert, update
NEW.mod_op := left(TG_OP, 1);
NEW.mod_datetime := now();
RETURN NEW;
END IF;
END
$func$ LANGUAGE plpgsql;
In the ELSE branch just assign to NEW directly. No need for more dynamic SQL - which would fire the same trigger again causing an endless loop. That's the primary error.
RETURN NEW; outside the IF construct would break your trigger function for DELETE, since NEW is not assigned for DELETEs.
A key feature is the use of hstore and the hstore operator #= to dynamically change two selected fields of the well-known row type - that is unknown at the time of writing the code. This way you do not tamper with the original OLD value, which might have surprising side effect if you have more triggers down the chain of events.
OLD #= hstore('{mod_op, mod_datetime}'::text[]
, ARRAY[left(TG_OP, 1), now()::text]);
The additional module hstore must be installed. Details:
How to set value of composite variable field using dynamic SQL
Passing column names dynamically for a record variable in PostgreSQL
Using the hstore(text[], text[]) variant here to construct an hstore value with multiple fields on the fly.
The assignment operator in plpgsql is :=:
The forgotten assignment operator "=" and the commonplace ":="
Note that I used the column name mod_datetime instead of the misleading mod_date, since the column is obviously a timestamp and not a date.
I added a couple of other improvements while being at it. And the trigger itself should look like this:
CREATE TRIGGER insupdel_bef
BEFORE INSERT OR UPDATE OR DELETE ON table_name
FOR EACH ROW EXECUTE PROCEDURE sample_trigger_func();
SQL Fiddle.

Getting all columns name in an table using trigger function in postgresql

How can I get all column names and their values in trigger function because i need to validate all column values before inserting into the table.I have tried below code.If we know that column name means we can get the value easily by using NEW object in trigger function as NEW.myColumnName.But here I need to get the column name dynamically...
CREATE FUNCTION insert_update_validate() RETURNS TRIGGER AS $$
DECLARE
BEGIN
FOR i IN 0..(TG_ARGV-1) LOOP
IF TG_ARGV[i] IS NULL THEN
RAISE EXCEPTION 'cannot have null VALUE', NEW.TG_ARGV[i];
END LOOP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Since hstore is part of PostgreSQL, casting a row to hstore is the primary method to iterate on its columns, at least in a plpgsql context. Otherwise as a language it doesn't provide any construct to extract column names from rows.
Basically it's about iterating over each(store(NEW)). Here's a skeleton you may use:
CREATE FUNCTION insert_update_validate() RETURNS TRIGGER AS $$
DECLARE
k text;
v text;
BEGIN
FOR k,v IN select key,value from each(hstore(NEW)) LOOP
if v is null then
raise exception 'value is null for column %', k;
end if;
END LOOP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;