Postgres dynamic insert in trigger function - postgresql

I am trying to create a trigger function that insert a new record to the audit tables dynamically.
CREATE OR REPLACE FUNCTION schema.table()
RETURNS trigger
LANGUAGE 'plpgsql'
COST 100
VOLATILE NOT LEAKPROOF
AS $BODY$
DECLARE
_tablename text;
BEGIN
_tablename := 'user_audit';
EXECUTE 'INSERT INTO audit.' || quote_ident(_tablename) || ' VALUES ($1.*)' USING OLD;
RETURN NEW;
END;
$BODY$;
Trigger function above works fine as it take everything in OLD and inserts it into the audit table as expected. However I have a tstzrange range column in my tables called timestampzt_range and what I need to do is set the value for it in audit table using LOWER(OLD.timestampzt_range), LOWER(NEW.timestampzt_range). How can I achieve this dynamically without using insert statement like below as I would like to use this trigger function on multiple tables.
INSERT INTO audit.user_audit
(
column_1,
column_2,
timestampzt_range
)
VALUES
(
OLD.column_1,
OLD.column_2,
tstzrange(LOWER(OLD.timestampzt_range), LOWER(NEW.timestampzt_range))
);
I only need to use this on update and table name will be passed as a parameter to the trigger function if I can achieve dynamic statement. Only the audit columns are consistent across the entire database so it is important for me create insert using OLD or somehow dynamically extract everything from it but the timestampzt_range and then use tstzrange(LOWER(OLD.timestampzt_range), LOWER(NEW.timestampzt_range)) as value for the range column.

First comment : you can use the NEW and OLD key words only in an UPDATE trigger. In an INSERT trigger, OLD doesn't exist. In a DELETE trigger, NEW doesn't exist. See the manual.
Then you can replace in your trigger function
'INSERT INTO audit.' || quote_ident(_tablename) || ' VALUES ($1.*)' USING OLD
by
'INSERT INTO audit.' || quote_ident(_tablename) || ' VALUES (OLD.column_1, OLD.column_2, tstzrange(LOWER(OLD.timestampzt_range), LOWER(NEW.timestampzt_range)))'
so that to achieve your expected result.
Last but not least, your dynamic statement EXECUTE 'INSERT INTO audit.' ... is useless because _tablename is a static parameter declared inside the function.

Related

Loop over NEW Record

Writing an audit trigger. Inside the postgresql function I'm trying todo:
'INSERT INTO ' || able_name || ' (' || columns || ') VALUES ' || NEW || ';'
When NEW is turned into string, varchar variables will not have quotes around them. This will cause the insert to fail. Easier would be to turn all the column values of NEW into varchar values, and postgres would automatically cast them into right values - when INSERT is executed.
Can I loop over the NEW record without turning it into json?
Looking around, I couldn't find good resource explaining how to work with Postgres Record type.
If your target table's structure is identical to the new structure, you don't really need to iterate over the columns.
Something like this will work:
create function audit_trigger()
returns trigger
as
$$
declare
l_columns text;
l_table_name text;
begin
-- this builds the name of the target table dynamicall
l_table_name := tg_table_name||'_audit';
execute format('insert into %I select ($1).*', l_table_name) using new;
return new;
end;
$$
language plpgsql;
Even if you don't want to store the changed data as a JSONB column, you can still use JSON functions to iterate over the columns of the new record if think you need it nevertheless.
The following will store the list of column names of the new record in the variable l_columns:
select string_agg(quote_ident(col), ',')
into l_columns
from jsonb_each_text(to_jsonb(new)) as t(col, val);

Fill a variable with "array_to_string" in a plpgsql trigger function

I'm working with PostgreSQL 9.5.
I'm creating a trigger in PL/pgSQL, that adds a record to a table (synthese_poly) when an INSERT is performed on a second table (operation_poly), with other tables data.
The trigger works well, except for some variables, that are not filled (especially the ones I try to fill with an array_to_string() function).
This is the code:
-- Function: bdtravaux.totablesynth_fn()
-- DROP FUNCTION bdtravaux.totablesynth_fn();
CREATE OR REPLACE FUNCTION bdtravaux.totablesynth_fn()
RETURNS trigger AS
$BODY$
DECLARE
varoperateur varchar;
varchantvol boolean;
BEGIN
IF (TG_OP = 'INSERT') THEN
varsortie_id := NEW.sortie;
varopeid := NEW.operation_id;
--The following « SELECT » queries take data in third-party tables and fill variables, which will be used in the final insertion query.
SELECT array_to_string(array_agg(DISTINCT oper.operateurs),'; ')
INTO varoperateur
FROM bdtravaux.join_operateurs oper INNER JOIN bdtravaux.operation_poly o ON (oper.id_joinop=o.id_oper)
WHERE o.operation_id = varopeid;
SELECT CASE WHEN o.ope_chvol = 0 THEN 'f' ELSE 't' END as opechvol INTO varchantvol
FROM bdtravaux.operation_poly o WHERE o.operation_id = varopeid;
-- «INSERT» query
INSERT INTO bdtravaux.synthese_poly (soperateur, schantvol) SELECT varoperateur, varchantvol;
RAISE NOTICE 'varoperateur value : (%)', varoperateur;
RAISE NOTICE 'varchantvol value : (%)', varchantvol;
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION bdtravaux.totablesynth_fn()
OWNER TO postgres;
And this is the trigger :
-- Trigger: totablesynth on bdtravaux.operation_poly
-- DROP TRIGGER totablesynth ON bdtravaux.operation_poly;
CREATE TRIGGER totablesynth
AFTER INSERT
ON bdtravaux.operation_poly
FOR EACH ROW
WHEN ((new.chantfini = true))
EXECUTE PROCEDURE bdtravaux.totablesynth_fn();
The varchantvol variable is correctly filled, but varoperateur stays desperately empty (NULL value) (and so on for the corresponding field in the synthese_poly table).
Note:
The SELECT array_to_string(…) ... query itself (launched with pgAdmin, without INTO varoperateur and replacing varopeid with a value) works well, and returns a string.
I tried to change array_to_string() function and variables' data types (using ::varchar or ::text …), nothing works.
Do you see what can happen?
using array_agg
You can replace array_to_string(array_agg(DISTINCT oper.operateurs),'; ') with
string_agg(DISTINCT oper.operateurs,'; ')
And you can use order by to sort the text in the agregate
string_agg(DISTINCT oper.operateurs,'; ' ORDER BY oper.operateurs)
My educated guess: you have a trigger with BEFORE INSERT ON bdtravaux.operation_poly. And operation_id is its serial PK column.
In this case, the query with WHERE o.operation_id = varopeid
(where varopeid has been filled with NEW.operation_id) can never find any rows because the row is not in the table, yet.
array_agg() has no role in this.
Would work with a trigger AFTER INSERT ON bdtravaux.operation_poly. But if id_oper is from the same inserted row, you can just simplify to:
SELECT array_to_string(array_agg(DISTINCT oper.operateurs),'; ')
INTO varoperateur
FROM bdtravaux.join_operateurs oper
WHERE oper.id_joinop = NEW.id_oper;
And keep the BEFORE trigger.
The whole function might be simpler, can probably done with a single query.

Creating a trigger for child table insertion returns confusing error

I am trying to write a trigger function that will input values into separate child tables, however I am getting an error I have not seen before.
Here is an example set up:
-- create initial table
CREATE TABLE public.testlog(
id serial not null,
col1 integer,
col2 integer,
col3 integer,
name text
);
-- create child table
CREATE TABLE public.testlog_a (primary key(id)) INHERITS(public.testlog);
-- make trigger function for insert
CREATE OR REPLACE FUNCTION public.test_log() RETURNS trigger AS
$$
DECLARE
qry text;
BEGIN
qry := 'INSERT INTO public.testlog_' || NEW.name || ' SELECT ($1).*';
EXECUTE qry USING NEW.*;
RETURN OLD;
END
$$
LANGUAGE plpgsql VOLATILE SECURITY DEFINER;
-- add function to table
CREATE TRIGGER test_log_sorter BEFORE INSERT
ON public.testlog FOR EACH ROW
EXECUTE PROCEDURE public.test_log();
and the query:
INSERT INTO public.testlog (col1, col2, col3, name) values (1, 2, 3, 'a');
error message:
[Err] ERROR: query "SELECT NEW.*" returned 5 columns
CONTEXT: PL/pgSQL function test_log() line 7 at EXECUTE statement
5 columns is exactly what I am looking for it to return, so clearly there is something I am not understanding but the error message seems to make no sense.
Can anybody explain why I am getting this?
Your solution fixes the passing of the row-typed NEW variable. However, you have a sneaky SQL-injection hole in your code, that's particularly dangerous in a SECURITY DEFINER function. User input must never be converted to SQL code unescaped.
Sanitize like this:
CREATE OR REPLACE FUNCTION trg_test_log()
RETURNS trigger AS
$$
BEGIN
EXECUTE 'INSERT INTO public.' || quote_ident('testlog_' || NEW.name)
|| ' SELECT ($1).*'
USING NEW;
RETURN NULL;
END
$$
LANGUAGE plpgsql SECURITY DEFINER;
Also:
OLD is not defined in an INSERT trigger.
You don't need a variable. Assignments are comparatively expensive in plpgsql.
The EXECUTE qry USING NEW.* passes in the NEW.* as the arguments to the query. Since NEW.* returns five columns, the query should have $1, $2, $3, $4 and $5 in order to bind the five columns.
You are expecting a single argument ($1) which has five columns in it. I believe that if you change the the line to
EXECUTE qry USING NEW;
it will work as you expect.
With regards to Robert M. Lefkowitz' response, the answer is so simple: NEW as opposed to NEW.*
CREATE OR REPLACE FUNCTION public.test_log() RETURNS trigger AS
$$
DECLARE
qry text;
BEGIN
qry := 'INSERT INTO public.testlog_' || NEW.name || ' SELECT ($1).*';
EXECUTE qry USING NEW;
RETURN OLD;
END
$$
LANGUAGE plpgsql VOLATILE SECURITY DEFINER
COST 100;
thanks.

how to circumvent missing record type on insert

I'd like to make a copy of a row in one table addressed by a field in another table, like this:
CREATE OR REPLACE FUNCTION f_ins_up_vorb()
RETURNS TRIGGER AS $$
DECLARE
dienst dienst%ROWTYPE;
account record;
BEGIN
-- ...
EXECUTE format('SELECT * FROM %s WHERE id=$1',dienst.tabelle)
USING NEW.id INTO account;
EXECUTE 'INSERT INTO ' || dienst.tabelle || 'shadow SELECT ($1).*, now(), $2' USING account, jobid;
RETURN NEW;
END
$$ LANGUAGE plpgsql;
But this yields:
ERROR: record type has not been registered
CONTEXT: SQL statement "INSERT INTO accountadshadow SELECT ($1).*, now(), $2"
PL/pgSQL function f_ins_up_vorb() line 30 at EXECUTE statement
The tables addressed by dienst.tabelle have no common type but the target table (dienst.tabelle || 'shadow') is always a superset of the source table. So this should always work (and does work in a trigger function, where I use NEW, which seems to have a record type).
Is there any way around this?
Try something like:
CREATE OR REPLACE FUNCTION f_ins_up_vorb()
RETURNS TRIGGER AS $$
DECLARE
dienst dienst%ROWTYPE;
BEGIN
-- ...
EXECUTE 'INSERT INTO '||dienst.tabelle||'shadow
SELECT *, now(), $2
FROM '||dienst.tabelle||'
WHERE id=$1'
USING NEW.id, jobid;
RETURN NEW;
END
$$ LANGUAGE plpgsql;
If you are trying to create some kind of log trigger - read this page first.

a postgres update trigger performs everything else except the actual update

Let's use a test table :
CREATE TABLE labs.date_test
(
pkey int NOT NULL,
val integer,
date timestamp without time zone,
CONSTRAINT date_test_pkey PRIMARY KEY (pkey)
);
I have a trigger function defined as below. It is a function to insert a date into a specified column in the table. Its arguments are the primary key, the name of the date field, and the date to be inserted:
CREATE OR REPLACE FUNCTION tf_set_date()
RETURNS trigger AS
$BODY$
DECLARE
table_name text;
pkey_col text := TG_ARGV[0];
date_col text := TG_ARGV[1];
date_val text := TG_ARGV[2];
BEGIN
table_name := format('%I.%I', TG_TABLE_SCHEMA, TG_TABLE_NAME);
IF TG_NARGS != 3 THEN
RAISE 'Wrong number of args for tf_set_date()'
USING HINT='Check triggers for table ' || table_name;
END IF;
EXECUTE format('UPDATE %s SET %I = %s' ||
' WHERE %I = ($1::text::%s).%I',
table_name, date_col, date_val,
pkey_col, table_name, pkey_col )
USING NEW;
RAISE NOTICE '%', NEW;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
The actual trigger definition is as follows:
CREATE TRIGGER t_set_ready_date
BEFORE UPDATE OF val
ON labs.date_test
FOR EACH ROW
EXECUTE PROCEDURE tf_set_date('pkey', 'date', 'localtimestamp(0)');
Now say I do: INSERT INTO TABLEdate_test(pkey) values(1);`
Then I perform an update as follows:
UPDATE labs.date_test SET val = 1 WHERE pkey = 1;
Now the date gets inserted as expected. But the val field is still NULL. It does not have 1 as one would expect (or rather as I expected).
What am I doing wrong? The RAISE NOTICE in the trigger shows that NEW is still what I expect it to be. Aren't UPDATEs allowed in BEFORE UPDATE triggers? One comment about postgres triggers seems to indicate that original the UPDATE gets overwritten if there is an UPDATE statement in a BEFORE UPDATE trigger. Can someone help me out?
EDIT
I am trying to update the same table that invoked the trigger, and that too the same row which is to be modified by the UPDATE statement that invoked the trigger. I am running Postgresql 9.2
Given all the dynamic table names it isn't entirely clear if this trigger issues an update on the same table that invoked the trigger.
If so: That won't work. You can't UPDATE some_table in a BEFORE trigger on some_table. Or, more strictly, you can, but if you update any row that is affected by the statement that's invoking the trigger results will be unpredictable so it isn't generally a good idea.
Instead, alter the values in NEW directly. You can't do this with dynamic column names, unfortunately; you'll just have to customise the trigger or use an AFTER trigger to do the update after the rows have already been changed.
I am not sure, but your triggers can do recursion calls - it does UPDATE same table from UPDATE trigger. This is usually bad practice, and usually is not good idea to write too generic triggers. But I don't know what you are doing, maybe you need it, but you have to be sure, so you are protected against recursion.
For debugging of triggers is good push to start and to end of function body debug messages. Probably you use GET DIAGNOSTICS statement after EXECUTE statement for information about impact of dynamic SQL
DECLARE
_updated_rows int;
_query text;
BEGIN
RAISE NOTICE 'Start trigger function xxx';
...
_query := format('UPDATE ....);
RAISE NOTICE 'dynamic sql %, %', _query, new;
EXECUTE _query USING new;
GET DIAGNOSICS _updated_rows = ROW_COUNT;
RAISE NOTICE 'Updated rows %', _updated_rows;
...