To ignore result in BEFORE TRIGGER of PostgreSQL? - postgresql

This thread is a part challenge of this thread to which I am searching a better solution for one part by BEFORE TRIGGER.
I just want to launch a trigger to convert to correct brackets.
I am thinking whether I should return from the trigger NULL or something else in before trigger.
Code
CREATE OR REPLACE FUNCTION insbef_events_function()
RETURNS TRIGGER AS
$func$
DECLARE
m int[];
BEGIN
FOREACH m SLICE 1 IN ARRAY TG_ARGV[0]::int[]
LOOP
INSERT INTO events (measurement_id, event_index_start, event_index_end)
SELECT NEW.measurement_id, m[1], m[2]; -- Postgres array starts with 1 !
END LOOP;
-- do something with _result ...
RETURN NULL; -- result ignored since this is an BEFORE trigger TODO right?
END
$func$ LANGUAGE plpgsql;
which I use the by the function
CREATE OR REPLACE FUNCTION f_create_my_trigger_events(_arg1 int, _arg2 text, _arg3 text)
RETURNS void AS
$func$
BEGIN
EXECUTE format($$
DROP TRIGGER IF EXISTS insbef_ids ON events
CREATE TRIGGER insbef_ids
BEFORE INSERT ON events
FOR EACH ROW EXECUTE PROCEDURE insbef_events_function(%1$L)$$
, translate(_arg2, '[]', '{}'), translate(_arg3, '[]', '{}')
);
END
$func$ LANGUAGE plpgsql;
I am unsure about this line: RETURN NULL; -- result ignored since this is anBEFOREtrigger TODO right?, since I think this is the case in AFTER trigger but not in before trigger.
I just want to launch a trigger to convert correct brackets.
Test command is sudo -u postgres psql detector -c "SELECT f_create_my_trigger_events(1,'[112]','[113]');" getting the following error because of misunderstanding of the returning -thing, I think.
LINE 3: CREATE TRIGGER insbef_ids
^
QUERY:
DROP TRIGGER IF EXISTS insbef_ids ON events
CREATE TRIGGER insbef_ids
BEFORE INSERT ON events
FOR EACH ROW EXECUTE PROCEDURE insbef_events_function('{112}')
CONTEXT: PL/pgSQL function f_create_my_trigger_events(integer,text,text) line 4 at EXECUTE statement
How can you manage BEFORE triggers in PostgreSQL 9.4?

First of all, you need to pass the row variable in a BEFORE trigger. Passing NULL cancels the operation for the row:
CREATE OR REPLACE FUNCTION insbef_events_function()
RETURNS TRIGGER AS
$func$
DECLARE
m int[];
BEGIN
FOREACH m SLICE 1 IN ARRAY TG_ARGV[0]::int[]
LOOP
INSERT INTO events (measurement_id, event_index_start, event_index_end)
SELECT NEW.measurement_id, m[1], m[2]; -- Postgres array subscripts start with 1
END LOOP;
-- do something with _result ...
RETURN NEW; -- NULL would cancel operation in BEFORE trigger!
END
$func$ LANGUAGE plpgsql;
I demonstrated the use of RETRUN NULL in an AFTER trigger in my previous answer. You can't do the same for a BEFORE trigger. The manual:
Row-level triggers fired BEFORE can return null to signal the trigger
manager to skip the rest of the operation for this row (i.e.,
subsequent triggers are not fired, and the INSERT/UPDATE/DELETE does
not occur for this row). If a nonnull value is returned then the
operation proceeds with that row value.
There is more. Read the manual.
But since you are passing two 1-dimensional arrays instead of one 2-dimensional array now, you need to adapt your trigger logic:
CREATE OR REPLACE FUNCTION insbef_events_function()
LANGUAGE plpgsql RETURNS TRIGGER AS
$func$
DECLARE
a1 int[] := TG_ARGV[1]::int[];
a2 int[] := TG_ARGV[2]::int[];
BEGIN
FOR i in array_lower(a1, 1) .. array_upper(a1, 1)
LOOP
INSERT INTO events (measurement_id, event_index_start, event_index_end)
SELECT NEW.measurement_id -- or TG_ARGV[0]::int instead?
, a1[i], a2[i];
END LOOP;
RETURN NEW; -- NULL would cancel operation in BEFORE trigger!
END
$func$;
It's your responsibility that both arrays have the same number of elements.
The function changing the trigger could look like this now:
CREATE OR REPLACE FUNCTION f_create_my_trigger_events(_arg1 int, _arg2 text, _arg3 text)
LANGUAGE plpgsql RETURNS void AS
$func$
BEGIN
EXECUTE format(
$$DROP TRIGGER IF EXISTS insbef_ids ON measurements; -- on measurements ..
CREATE TRIGGER insbef_ids
BEFORE INSERT ON measurements -- .. according to previous posts!!
FOR EACH ROW EXECUTE PROCEDURE insbef_events_function(%s, %L, %L)$$
, _arg1
, translate(_arg2, '[]', '{}')
, translate(_arg3, '[]', '{}')
);
END
$func$;
You need to understand basics of SQL, PL/pgSQL, trigger functions and array handling before using this advanced automated design.

Related

postgresql - trigger function with condition

I'm trying to create a trigger with a condition. Based on the geom length I want the attribute "nom" (= name) to be written in upper case or lower case.
here's what I have:
CREATE OR REPLACE FUNCTION public.test_upper_lower()
RETURNS trigger AS
$BODY$
BEGIN
NEW.dummy:= (ST_Length(new.geom));
if (SELECT dummy FROM ligne_ligne)>100
then NEW.nom:= LOWER(nom) FROM ligne_ligne;
else NEW.nom:= UPPER(nom) FROM ligne_ligne;
end if;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
DROP trigger IF EXISTS test_upper_lower on public.ligne_ligne;
CREATE trigger test_upper_lower BEFORE INSERT OR UPDATE on public.ligne_ligne
FOR EACH ROW
EXECUTE PROCEDURE public.test_upper_lower();
With this I have a "more than one row returned by a subquery" error
Based on other questions on this forum I tried it using case instead of if and using when in the trigger itself not the function but neither are working
Any ideas ?
Thanks
You don't need (or can actually) use SELECT statements to access data from the inserted row.
The part SELECT dummy FROM ligne_ligne returns all rows from that table - not just from the one relevant to the trigger.
As you just want to check the value you just calculated, simply use new.dummy at that point:
CREATE OR REPLACE FUNCTION public.test_upper_lower()
RETURNS trigger AS
$BODY$
BEGIN
NEW.dummy:= ST_Length(new.geom);
if new.dummy > 100 then --<< no SELECT necessary
NEW.nom:= LOWER(new.nom); --<< no "FROM", just access the value
else
NEW.nom:= UPPER(new.nom);
end if;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;

Postges call procedure for each row of a returning delete statement

I have a delete statement and I want to call a procedure for each of the rows affected by it. Currently I only seem to be able to access a single row of the set that was deleted.
That is if the delete statement below affects multiple rows then I only see a single passed to my analytics procedure
create or replace function platform.update_foo() returns trigger language plpgsql as $$
declare
result record;
begin
with affected_rows as (
delete from private.foo
where environment_id=new.environment_id
and name=new.name
returning *
)
select private.analytics(affected_rows, 'cancelled')
from affected_rows into result;
return new;
end;
$$;
My analytics function has a signature like so
analytics(f private.foo, t varchar default null)
returns void as $$
begin
-- ...

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.

PostgreSQL increment serial column on update

I am trying to increment a serial column after an update. I have written a trigger function to help.
CREATE FUNCTION "public"."update_transaction_id" () RETURNS trigger AS
$BODY$
DECLARE
curr_id integer;
BEGIN
curr_id = nextval(pg_get_serial_sequence('current_table', 'transaction_id'));
NEW.transaction_id = curr_id;
PERFORM SETVAL((SELECT pg_get_serial_sequence('current_table', 'transaction_id')), curr_id + 1, false);
RETURN NEW;
END;
$BODY$ LANGUAGE 'plpgsql' IMMUTABLE CALLED ON NULL INPUT SECURITY INVOKER;
CREATE TRIGGER "trg_update_transaction_id" AFTER UPDATE
ON "current_table" FOR EACH ROW
EXECUTE PROCEDURE "public"."update_transaction_id"();
So the column is transaction_id in the table current_table. The query executes fine and the my update function does run through this trigger. However, the transaction_id column remains the same value. Is there something wrong with this procedure?
There are three errors in your code. First, you use the equality operator = instead of the assignment operator :=. Second, you should not SETVAL the sequence; it knows perfectly well how to maintain itself. Third, this function should be called by a BEFORE UPDATE trigger or the assignment to transaction_id will not persist.
So why not simply:
CREATE FUNCTION update_transaction_id() RETURNS trigger AS $BODY$
BEGIN
NEW.transaction_id := nextval('seq_name');
RETURN NEW;
END;
$BODY$ LANGUAGE 'plpgsql';
You can trivially retrieve the immutable name of the sequence from the table definition, so plug that in straightaway.

Create a function to get column from multiple tables in PostgreSQL

I'm trying to create a function to get a field value from multiple tables in my database. I made script like this:
CREATE OR REPLACE FUNCTION get_all_changes() RETURNS SETOF RECORD AS
$$
DECLARE
tblname VARCHAR;
tblrow RECORD;
row RECORD;
BEGIN
FOR tblrow IN SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname='public' LOOP /*FOREACH tblname IN ARRAY $1 LOOP*/
RAISE NOTICE 'r: %', tblrow.tablename;
FOR row IN SELECT MAX("lastUpdate") FROM tblrow.tablename LOOP
RETURN NEXT row;
END LOOP;
END LOOP;
END
$$
LANGUAGE 'plpgsql' ;
SELECT get_all_changes();
But it is not working, everytime it shows this error
tblrow.tablename" not defined in line "FOR row IN SELECT MAX("lastUpdate") FROM tblrow.tablename LOOP"
Your inner FOR loop must use the FOR...EXECUTE syntax as shown in the manual:
FOR target IN EXECUTE text_expression [ USING expression [, ... ] ] LOOP
statements
END LOOP [ label ];
In your case something along this line:
FOR row IN EXECUTE 'SELECT MAX("lastUpdate") FROM ' || quote_ident(tblrow.tablename) LOOP
RETURN NEXT row;
END LOOP
The reason for this is explained in the manual somewhere else:
Oftentimes you will want to generate dynamic commands inside your PL/pgSQL functions, that is, commands that will involve different tables or different data types each time they are executed. PL/pgSQL's normal attempts to cache plans for commands (as discussed in Section 39.10.2) will not work in such scenarios. To handle this sort of problem, the EXECUTE statement is provided[...]
Answer to your new question (mislabeled as answer):
This can be much simpler. You do not need to create a table just do define a record type.
If at all, you would better create a type with CREATE TYPE, but that's only efficient if you need the type in multiple places. For just a single function, you can use RETURNS TABLE instead :
CREATE OR REPLACE FUNCTION get_all_changes(text[])
RETURNS TABLE (tablename text
,"lastUpdate" timestamp with time zone
,nums integer) AS
$func$
DECLARE
tblname text;
BEGIN
FOREACH tblname IN ARRAY $1 LOOP
RETURN QUERY EXECUTE format(
$f$SELECT '%I', MAX("lastUpdate"), COUNT(*)::int FROM %1$I
$f$, tblname)
END LOOP;
END
$func$ LANGUAGE plpgsql;
A couple more points:
Use RETURN QUERY EXECUTE instead of the nested loop. Much simpler and faster.
Column aliases would only serve as documentation, those names are discarded in favor of the names declared in the RETURNS clause (directly or indirectly).
Use format() with %I to replace the concatenation with quote_ident() and %1$I to refer to the same parameter another time.
count() usually returns type bigint. Cast the integer, since you defined the column in the return type as such: count(*)::int.
Thanks,
I finally made my script like:
CREATE TABLE IF NOT EXISTS __rsdb_changes (tablename text,"lastUpdate" timestamp with time zone, nums bigint);
CREATE OR REPLACE FUNCTION get_all_changes(varchar[]) RETURNS SETOF __rsdb_changes AS /*TABLE (tablename varchar(40),"lastUpdate" timestamp with time zone, nums integer)*/
$$
DECLARE
tblname VARCHAR;
tblrow RECORD;
row RECORD;
BEGIN
FOREACH tblname IN ARRAY $1 LOOP
/*RAISE NOTICE 'r: %', tblrow.tablename;*/
FOR row IN EXECUTE 'SELECT CONCAT('''|| quote_ident(tblname) ||''') AS tablename, MAX("lastUpdate") AS "lastUpdate",COUNT(*) AS nums FROM ' || quote_ident(tblname) LOOP
/*RAISE NOTICE 'row.tablename: %',row.tablename;*/
/*RAISE NOTICE 'row.lastUpdate: %',row."lastUpdate";*/
/*RAISE NOTICE 'row.nums: %',row.nums;*/
RETURN NEXT row;
END LOOP;
END LOOP;
RETURN;
END
$$
LANGUAGE 'plpgsql' ;
Well, it works. But it seems I can only create a table to define the return structure instead of just RETURNS SETOF RECORD. Am I right?
Thanks again.