Error in PostgreSQL trigger - postgresql

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)

Related

PostgreSQL - Before Update Trigger determine which columns were updated

I have a table with many columns.
Need to find which columns value have changed.
I am using the following code. I am having a performance problem.
Can I do this with a different method?
CREATE OR REPLACE FUNCTION public.mytable()
RETURNS trigger AS $$
DECLARE
_rec record;
BEGIN
FOR _rec IN
SELECT
o.key
, o.value AS old_value
, n.value AS new_value
FROM json_each(to_json(new)) n
INNER JOIN json_each(to_json(old)) o ON o.key = n.key
LOOP
IF (_rec.old_value::text IS DISTINCT FROM _rec.new_value::text) THEN
/*
field name: _rec.key,
old values: _rec.old_value,
new value : _rec.new_value
*/
END IF;
END LOOP;
RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';

How to pass NEW.* to EXECUTE in trigger function

I have a simple mission is inserting huge MD5 values into tables (partitioned table), and have created a trigger and also a trigger function to instead of INSERT operation. And in function I checked the first two characters of NEW.md5 to determine which table should be inserted.
DECLARE
tb text;
BEGIN
IF TG_OP = 'INSERT' THEN
tb = 'samples_' || left(NEW.md5, 2);
EXECUTE(format('INSERT INTO %s VALUES (%s);', tb, NEW.*)); <- WRONG
END IF;
RETURN NULL;
END;
The question is how to concat the NEW.* into the SQL statement?
Best with the USING clause of EXECUTE:
CREATE FUNCTION foo ()
RETURNS trigger AS
$func$
BEGIN
IF TG_OP = 'INSERT' THEN
EXECUTE format('INSERT INTO %s SELECT $1.*'
, 'samples_' || left(NEW.md5, 2);
USING NEW;
END IF;
RETURN NULL;
END
$func$ LANGUAGE plpgsql;
And EXECUTE does not require parentheses.
And you are aware that identifiers are folded to lower case unless quoted where necessary (%I instead of %s in format()).
More details:
INSERT with dynamic table name in trigger function
How to dynamically use TG_TABLE_NAME in PostgreSQL 8.2?

Can I iterate over NEW in a PostgreSQL trigger function?

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?

PostgreSQL Cursor Error

I have the code:
DECLARE
cliente_cursor CURSOR FOR SELECT * FROM cliente;
cliente cliente.id_clie%TYPE;
nom cliente.nom_clie%TYPE;
BEGIN
OPEN cliente_cursor;
FETCH cliente_cursor INTO cliente, nom;
But I cannot run it. The following error appears:
ERROR: syntax error at or near "cliente"
LINE 3: cliente cliente.id_clie% TYPE;
        ^
I have the table "cliente" which has:
I want to create a cursor that shows only the content of the columns: id_clie, nom_clie of the previous table.
I have also used:
create or replace function facturas_cliente()
returns table (a int, b character varying(40))
language plpgsql as $$
declare
x cliente%rowtype ;
cliente_cursor cursor for select id_clie, nom_clie from cliente
order by id_clie;
begin
for x in cliente_cursor loop
return next x;
end loop;
end $$;
But I get the error:
ERROR: RETURN NEXT can not have parameters in a function with OUT parameters
LINE 9: return next x;
                    ^
What am I doing wrong?
First issue is strange. I tested on PostgreSQL 9.5 (but same code should to work on 9.2 and newer):
CREATE TABLE cliente(id_clie int, tel_clie varchar(15), dir_clie varchar(15));
DO $$
DECLARE
cliente_cursor CURSOR FOR SELECT * FROM cliente;
cliente cliente.id_clie%TYPE;
tel cliente.tel_clie%TYPE;
BEGIN
OPEN cliente_cursor;
END;
$$;
And it works without any issue. It looks like some mistyped error.
Second issue is clear. When function has a OUT variables or is declared as TABLE function, then RETURN NEXT has to be without expression. Returned composite value is based on actual context of OUT variables (columns declared in TABLE clause are OUT variables too). Your code should to be:
create or replace function facturas_cliente()
returns table (a int, b character varying(40))
language plpgsql as $$
declare
cliente_cursor cursor for select id_clie, nom_clie
from cliente
order by id_clie;
r record;
begin
for r in cliente_cursor -- a,b ~ OUT var declared in TABLE() clause
loop
a := r.id_clie; b := r.nom_clie;
return next; -- not: return next x;
end loop;
end $$;
This code can be reduced in PL/pgSQL two ways:
use a SQL function
create or replace function facturas_cliente()
returns table (a int, b character varying(40))
language sql as $$
select id_clie, nom_clie from cliente order by id_clie;
$$ language plpgsql;
use a RETURN QUERY statement in plpgsql:
create or replace function facturas_cliente()
returns table (a int, b character varying(40))
language sql as $$
BEGIN
RETURN QUERY SELECT id_clie, nom_clie
FROM cliente
ORDER BY id_clie;
RETURN;
END;
$$ language plpgsql;
Attention: these function can block a SQL optimizer if you use it in some complex query than trivial. Personally I don't like it. Use a view instead. It works like you need and there is no risk with optimization.
CREATE VIEW facturas_cliente
AS SELECT id_clie, nom_clie
FROM cliente
ORDER BY id_clie;

Dynamic upsert in postgresql

I have this upsert function that allows me to modify the fill_rate column of a row.
CREATE FUNCTION upsert_fillrate_alarming(integer, boolean) RETURNS VOID AS '
DECLARE
num ALIAS FOR $1;
dat ALIAS FOR $2;
BEGIN
LOOP
-- First try to update.
UPDATE alarming SET fill_rate = dat WHERE equipid = num;
IF FOUND THEN
RETURN;
END IF;
-- Since its not there we try to insert the key
-- Notice if we had a concurent key insertion we would error
BEGIN
INSERT INTO alarming (equipid, fill_rate) VALUES (num, dat);
RETURN;
EXCEPTION WHEN unique_violation THEN
-- Loop and try the update again
END;
END LOOP;
END;
' LANGUAGE 'plpgsql';
Is it possible to modify this function to take a column argument as well? Extra bonus points if there is a way to modify the function to take a column and a table.
As an alternative approach, you can do an upsert without a function by using an insert + update with where clauses to make them only succeed in the right case. E.g.
update mytable set col1='value1' where (col2 = 'myId');
insert into mytable select 'value1', 'myId' where not exists (select 1 from mytable where col2='myId');
Which would avoid having lots of custom postgres specific functions.
You want to read about dynamic commands in plsql.
Just build your query and invoke EXECUTE.
Maybe a simpler approach, just less line ;)
CREATE OR REPLACE FUNCTION upsert_tableName(arg1 type, arg2 type) RETURNS VOID AS $$
DECLARE
BEGIN
UPDATE tableName SET col1 = value WHERE colX = arg1 and colY = arg2;
IF NOT FOUND THEN
INSERT INTO tableName values (value, arg1, arg2);
END IF;
END;
$$ LANGUAGE 'plpgsql';