Trigger on every update or insert - sql

I want to create trigger that fires every time any column is changed - whether it is freshly updated or new insert. I created something like this:
CREATE TRIGGER textsearch
BEFORE INSERT OR UPDATE
ON table
FOR EACH ROW
EXECUTE PROCEDURE trigger();
and body of trigger() function is:
BEGIN
NEW.ts := (
SELECT COALESCE(a::text,'') || ' ' ||
COALESCE(b::int,'') || ' ' ||
COALESCE(c::text,'') || ' ' ||
COALESCE(d::int, '') || ' ' ||
COALESCE(e::text,'')
FROM table
WHERE table.id = new.id);
RETURN NEW;
END
I hope it is clear what I want to do.
My problem is that trigger fires only on update, not on insert. I guess that this isn't working because I have BEFORE INSERT OR UPDATE, but if I change it to AFTER INSERT OR UPDATE then it doesn't work neither for INSERT nor UPDATE.

you need to use the NEW record directly:
BEGIN
NEW.ts := concat_ws(' ', NEW.a::text, NEW.b::TEXT, NEW.c::TEXT);
RETURN NEW;
END;
The advantage of concat_ws over || is that concat_ws will treat NULL values differently. The result of 'foo'||NULL will yield NULL which is most probably not what you want. concat_ws will use an empty string NULL values.

It doesn't work because you're calling SELECT inside the function.
When it runs BEFORE INSERT then there isn't a row to select, is there?
Actually, BEFORE UPDATE you'll see the "old" version of the row anyway, won't it?
Just directly use the fields: NEW.a etc rather than selecting.
As an edit - here is an example showing what the trigger function can see. It's exaclty as you'd expect in a BEFORE trigger.
BEGIN;
CREATE TABLE tt (i int, t text, PRIMARY KEY (i));
CREATE FUNCTION trigfn() RETURNS TRIGGER AS $$
DECLARE
sv text;
BEGIN
SELECT t INTO sv FROM tt WHERE i = NEW.i;
RAISE NOTICE 'new value = %, selected value = %', NEW.t, sv;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigtest BEFORE INSERT OR UPDATE ON tt FOR EACH ROW EXECUTE PROCEDURE trigfn();
INSERT INTO tt VALUES (1,'a1');
UPDATE tt SET t = 'a2' WHERE i = 1;
ROLLBACK;

in your COALESCE statements when you cast b::int i had to change your coalesce to use integer place holder instead. As mentioned by a_horse_with_no_name this can end up with null values but you can see how to make your specific code example run. I included the "RAISE NOTICE" lines for debug purposes only.
Based on your provided information the following works for me:
CREATE TABLE my_table (id SERIAL NOT NULL,a TEXT,b INTEGER,c TEXT,d INTEGER,e TEXT);
CREATE OR REPLACE FUNCTION my_triggered_procedure() RETURNS trigger AS $$
BEGIN
if(TG_OP = 'UPDATE' OR TG_OP = 'INSERT') THEN
NEW.ts := (SELECT COALESCE(a::text,'') || ' ' ||
COALESCE(b::int,0) || ' ' ||
COALESCE(c::text,'') || ' ' ||
COALESCE(d::int, 0) || ' ' ||
COALESCE(e::text,'')
FROM my_table
WHERE id=NEW.id);
RAISE NOTICE 'INSERT OR UPDATE with new ts = %',NEW.ts;
RETURN NEW;
ELSIF (TG_OP = 'DELETE') THEN
OLD.ts := ' ';
RAISE NOTICE 'DELETED old id: %',OLD.id;
RETURN OLD;
END IF;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER text_search
AFTER INSERT OR UPDATE OR DELETE
ON my_table
FOR EACH ROW
EXECUTE PROCEDURE my_triggered_procedure();
INSERT INTO my_table (a,b,c,d,e) VALUES('text11',12,'text21',3,'text4');
>NOTICE: INSERT OR UPDATE with new ts = text11 12 text21 3 text4
>INSERT 0 1
DELETE FROM my_table WHERE id=24;
>NOTICE: DELETED ID = 24
>DELETE 1
PostgreSQL::Trigger Procedures

Related

PL/pgSQL select statement inside trigger returns null using where NEW

i'm creating a trigger that triggers on INSERT on a table,
and i wish to log the structure of tables inserted so i wrote this Function
CREATE OR REPLACE FUNCTION update_table_log_received()
RETURNS TRIGGER AS $$
DECLARE
added_column TEXT;
target_table_name TEXT;
old_column text;
BEGIN
-- Check if a new column has been added
IF (TG_OP = 'INSERT') THEN
added_column := NEW."COLUMN_NAME";
target_table_name := NEW."TABLE_NAME";
END IF;
SELECT column_name into old_column
FROM information_schema."columns"
WHERE table_schema = 'items'
and table_name = LOWER(NEW."TABLE_NAME")
and column_name = LOWER(NEW."COLUMN_NAME");
if (coalesce(old_column,'')='' or old_column='' or old_column = added_column) THEN
-- If a new column has been added
IF (Lower(added_column) != 'sync') then
-- Add the new column to the target table
EXECUTE 'ALTER TABLE items.' || LOWER(target_table_name)|| ' ADD COLUMN ' || LOWER(added_column) || ' VARCHAR(50)';
END IF;
end if;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
executed by this TRIGGER :
CREATE TRIGGER update_table_log_received_trigger
AFTER INSERT
ON items."TABLE_LOG_RECEIVED"
FOR EACH ROW
EXECUTE FUNCTION update_table_log_received();
the returned exception is the following :
! ERROR: the column « x » of the relation « y » already exists
Where: instruction SQL « ALTER TABLE items. ADD COLUMN x VARCHAR(50) »
my problem now is that it isn't supposed to pass the If checks (i pasted the code after many alterations i have two if conditions that do the same thing just because),
i debugged and logged the statements to note that the select query inside my function returns null apparently.
i also tried to use "USING NEW" but i am no expert so i couldn't make it work
is it a problem with the declared variable not being populated from the "NEW" record or am i executing the select statement wrong ?
EDIT : tl;dr for my problem, I would like to update a table in Database2 whenever the same table (that had the same structre) is altered from Database1, be it added column or changed column, at this point iI'm stuck at the first problem to add the column.
I am logging my tables' structures as strings into a new table and syncing that with Database2 to then have the trigger alter the same altered table from Database1, hope this makes more sense now.
Database1 log_table that logs all my tables' structures:
Database2 log_table_received that is a copy of log_table that executes
the trigger whenever new values are inserted;
Try this syntax:
CREATE OR REPLACE FUNCTION update_table_log_received()
RETURNS TRIGGER AS $$
DECLARE
added_column TEXT;
target_table_name TEXT;
old_column text;
BEGIN
-- Check if a new column has been added
IF (TG_OP = 'INSERT') THEN
added_column := new."column_name";
target_table_name := new."table_name";
END IF;
if not exists(select 1 from information_schema."columns" where table_name = target_table_name and column_name = added_column)
then
EXECUTE 'ALTER TABLE items.' || LOWER(target_table_name)|| ' ADD COLUMN ' || LOWER(added_column) || ' VARCHAR(50)';
end if;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
I tried on my DB this is works. You can change some details yourself.
Fixed; Question should have been :
How to select tables & table columns inside function in postgresql.
References:
How to add column if not exists on PostgreSQL?
How to check if a table exists in a given schema
How to get a list column names and datatypes of a table in PostgreSQL?
Basically information_schema can only be accessed by owner meaning the user or (i) see the result when i query it but it returns FALSE when executed inside a script more details here :
https://stackoverflow.com/a/24089729/15170264
Full trigger after fix with CTE to query the pg_catalog also added ADD COLUMN IF NOT EXISTS in my Execute query just to be safe
CREATE OR REPLACE FUNCTION update_table_log_received()
RETURNS TRIGGER AS $$
DECLARE
added_column TEXT;
target_table_name TEXT;
old_column varchar;
old_table varchar;
BEGIN
-- Check if a new column has been added
IF (TG_OP = 'INSERT') THEN
added_column := NEW."COLUMN_NAME";
target_table_name := NEW."TABLE_NAME";
END IF;
/*
* --------------- --CTE to find Columns of table "Target_table_name" from pg_catalog
*/
WITH cte_tables AS (
SELECT
pg_attribute.attname AS column_name,
pg_catalog.format_type(pg_attribute.atttypid, pg_attribute.atttypmod) AS data_type
FROM
pg_catalog.pg_attribute
INNER JOIN
pg_catalog.pg_class ON pg_class.oid = pg_attribute.attrelid
INNER JOIN
pg_catalog.pg_namespace ON pg_namespace.oid = pg_class.relnamespace
WHERE
pg_attribute.attnum > 0
AND NOT pg_attribute.attisdropped
AND pg_namespace.nspname = 'items'
AND pg_class.relname = 'trace'
ORDER BY
attnum ASC
)
select column_name into old_column from cte_tables where
column_name=LOWER(added_column);
if (old_column is null ) then
-- Add the new column to the target table
old_column := added_column;
EXECUTE 'ALTER TABLE items.' || LOWER(target_table_name)|| ' ADD COLUMN IF NOT EXISTS ' || LOWER(added_column) || ' VARCHAR(50)';
else
old_column := added_column || 'already exists ! ';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
create TRIGGER update_table_log_received_trigger
AFTER INSERT
ON items."TABLE_LOG_RECEIVED"
FOR EACH ROW
EXECUTE FUNCTION update_table_log_received();
Variable old_column stores the else condition message but i do not return it, would have if it was a simple function.

Postgresql insert trigger to concatenate

I want to create a trigger to concatenate my column with event "before insert", but the query didn't work and i got an error:
SQL error:
ERROR: syntax error at or near "SET" LINE 4: SET new.fullname =
CONCAT(new.first_name, '', new.mid_name, ...
In statement:
CREATE TRIGGER insert_trigger
BEFORE INSERT ON t_employees
FOR EACH ROW
SET new.fullname = CONCAT(new.first_name, '', new.mid_name, '', new.last_name);
Here's a working solution:
CREATE TEMPORARY TABLE t_employees (
first_name TEXT,
mid_name TEXT,
last_name TEXT,
fullname TEXT
);
CREATE OR REPLACE FUNCTION set_fullname()
RETURNS TRIGGER AS $$
BEGIN
NEW.fullname = NEW.first_name || ' ' || NEW.mid_name || ' ' || NEW.last_name;
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER set_fullname_trigger
BEFORE INSERT OR UPDATE ON t_employees
FOR EACH ROW
EXECUTE PROCEDURE set_fullname();
SET client_min_messages TO 'debug';
INSERT INTO t_employees VALUES ('fname1', 'mname1', 'lname1');
SELECT * FROM t_employees;
UPDATE t_employees SET first_name = 'updated-first-name';
SELECT * FROM t_employees;
Well, i finally did it. According your suggestions and postgresql docs i've made the trigger like what i want. Here is the syntax:
create the function:
CREATE FUNCTION insert_funct() RETURN TRIGGER AS
$$
BEGIN
SELECT new.fullname := CONCAT(new.first_name, '', new.mid_name, '', new.last_name);
END;
$$
LANGUAGE plpgsql;
then create the trigger:
CREATE TRIGGER insert_trigger
BEFORE INSERT ON t_employees
FOR EACH ROW
EXECUTE PROCEDURE insert_funct();

Postgresql: How to escape single quotes in Database trigger?

I created a database trigger to store the row data in an auditing table.
During the update operation, this trigger takes data from the main table and inserts it to a history table. (history table has columns: date, operation type say Update/Delete, actual row data)
But the trigger fails in some cases because of the quoted text in input data.
How can I escape the quoted text in my trigger?
--My trigger
CREATE OR REPLACE FUNCTION audit.if_modified() RETURNS TRIGGER AS $function$
DECLARE
temp_row RECORD; -- a temporary variable used on updates/deletes
v_sql text;
BEGIN
IF TG_WHEN <> 'AFTER' THEN
RAISE EXCEPTION 'audit.if_modified() may only run as an AFTER trigger';
END IF;
v_sql = 'select * from ' || TG_TABLE_NAME::regclass || '_history';
execute v_sql into temp_row;
select now() into temp_row.action_tstamp_tx;
temp_row.action = SUBSTRING(TG_OP,1,1);
IF (TG_OP = 'UPDATE' AND TG_LEVEL = 'ROW') THEN
temp_row.row_data = OLD;
ELSIF (TG_OP = 'DELETE' AND TG_LEVEL = 'ROW') THEN
temp_row.row_data = OLD;
ELSIF (TG_OP = 'INSERT' AND TG_LEVEL = 'ROW') THEN
temp_row.row_data = NEW;
ELSE
RAISE EXCEPTION '[audit.if_modified] - Trigger func added as trigger for unhandled case: %, %',TG_OP, TG_LEVEL;
RETURN NULL;
END IF;
EXECUTE 'INSERT INTO audit.' || TG_TABLE_NAME::regclass || '_history VALUES (''' ||
temp_row.action_tstamp_tx || ''',''' ||
temp_row.action || ''',''' ||
temp_row.row_data || ''')';
RETURN NULL;
END;
$function$
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = audit,public,pg_catalog;
This works fine for normal use cases but if the varchar data has single quoted text, then it fails to load data into history table.
ERROR: syntax error at or near "s"
LINE 1: ...VALUES ('2016-02-22 11:44:43.994295-06','U','(6,Tom's,"2016-02...
^
QUERY: INSERT INTO audit.test_history VALUES ('2016-02-22 11:44:43.994295-06','U','(6,Tom's,"2016-02-22 09:49:32.315543")')
CONTEXT: PL/pgSQL function if_modified() line 30 at EXECUTE
I am new to Postgresql. I tried with options like
regexp_replace() API
and
SELECT into temp_row.row_data unnest(('{' || trim((temp_row.row_data)::text, '()') || '}')::text[]);
etc but I couldn't understand how to loop through the ROWTYPE data and create the correct insert record.
Please share your thoughts on how can I edit my trigger to insert text with single quotes.
Thanks,
In general single, quotes are escaped by doubling them.
To put concatenate your variables into a SQL string, you should use quote_literal() - that function takes care of properly escaping single quote, e.g:
quote_literal(temp_row.row_data)
Having said that: the better (and safer) solution is to use parameters combined with format():
EXECUTE
format('INSERT INTO audit.%I_history values ($1, $2, $3)', tg_table_name)
using temp_row.action_tstamp_tx, temp_row.action, temp_row.row_data;
The %I placeholder usually takes care of properly escaping an identifier, although in this case it would not work. If you want to be 100% sure that even non-standard table names work properly, you need to first put the target table name into a variable and use that for the format() function:
l_tablename := TG_TABLE_NAME || '_history';
EXECUTE
format('INSERT INTO audit.%I_history values ($1, $2, $3)', l_tablename)
using ....
This part:
v_sql = 'select * from ' || TG_TABLE_NAME::regclass || '_history';
execute v_sql into temp_row;
is going to fail after the first row as well. execute .. into ... expects the query to return a single. The statement you are using will return all rows from the history table.
I also don't understand why you do that in the first place.
You don't need to select from the history table at all.
Something like this should be enough (untested!):
IF (TG_OP = 'UPDATE' AND TG_LEVEL = 'ROW') THEN
temp_row := OLD;
ELSIF (TG_OP = 'DELETE' AND TG_LEVEL = 'ROW') THEN
temp_row := OLD;
ELSIF (TG_OP = 'INSERT' AND TG_LEVEL = 'ROW') THEN
temp_row := NEW;
ELSE
RAISE EXCEPTION '[audit.if_modified] - Trigger func added as trigger for unhandled case: %, %',TG_OP, TG_LEVEL;
RETURN NULL;
END IF;
execute format ('insert ... values ($1, $2, $3')
using now(), SUBSTRING(TG_OP,1,1), temp_row;
Finally: audit triggers have been written before, and there are a lot of ready-made solutions for this:
Using hstore
Using jsonb
And a complex example from the Postgres Wiki

SQL Trigger time check + - 1hour [duplicate]

I am writing a simple trigger that is supposed to just send a message with the updated Count of rows as well as the old value of Gender and the updated value of Gender. When i run an update however I am getting the error that the table is mutating and the table might not be able to see it but I'm not exactly sure why.
trigger
create or replace trigger updatePERSONS
after update
on PERSONS
for each row
declare
n int;
oldGender varchar(20):= :OLD.Gender;
newGender varchar(20):= :NEW.Gender;
begin
select Count(*)
into n
from PERSONS;
if (oldGender != newGender) then
dbms_output.put_line('There are now '|| n || ' rows after update. Old gender: ' || oldGender
|| ', new Gender: ' || newGender);
end if;
End;
`
i know it has to do with the select statement after begin but how else would i get count of rows?
As #San points out, a row-level trigger on persons cannot generally query the persons table.
You'd need two triggers, a row-level trigger that can see the old and new gender and a statement-level trigger that can do the count. You could also, if you're using 11g, create a compound trigger with both row- and statement-level blocks.
create or replace trigger trg_stmt
after update
on persons
declare
l_cnt integer;
begin
select count(*)
into l_cnt
from persons;
dbms_output.put_line( 'There are now ' || l_cnt || ' rows.' );
end;
create or replace trigger trg_row
after update
on persons
for each row
begin
if( :new.gender != :old.gender )
then
dbms_output.put_line( 'Old gender = ' || :old.gender || ', new gender = ' || :new.gender );
end if;
end;

Can we restrict a trigger on table to only perform inserts based on function that is being called by it? (PostgreSQL)

Is there any way in PostgreSQL where we can restrict the trigger to only do the insert's based on the function that is being called? Say, I created a trigger on fact_activity but whenever I perform any insert's the trigger should execute the function to perform inserts on the table defined in the function only.
--Here's my trigger that executes the function.
CREATE TRIGGER test_insert
BEFORE INSERT ON fact_activity
FOR EACH ROW
EXECUTE PROCEDURE insert_function();
--Insert function
CREATE OR REPLACE FUNCTION insert_function()
RETURNS trigger AS
$BODY$
DECLARE
l_part_date TEXT;
l_table_name TEXT;
BEGIN
l_part_date := to_char(TO_date(NEW.activity_date_key ::text,'YYYYMMDD'),'YYYY_MM');
l_table_name := TG_TABLE_NAME ||'_'|| l_part_date;
EXECUTE 'INSERT INTO ' || l_table_name || ' SELECT $1.*' USING NEW;
RETURN NULL;
END;
$BODY$
LANGUAGE plpgsql;
Note: activity_date_key is of integer format (20150512). Hence the conversion.
The above script results in a record being inserted in both fact_activity & fact_activity_2015_05. But I only need to insert a record in fact_activity_2015_05.
--DDL for the tables:
CREATE TABLE fact_activity
(
gc_activity_key bigint NOT NULL DEFAULT nextval('fact_gc_activity_key_seq'::regclass),
user_key bigint,
user_category_key integer,
user_geographic_region_key integer,
activity_date_key integer
);
CREATE TABLE fact_activity_2015_05 ( )
INHERITS (fact_activity);
-- DML:
INSERT INTO fact_gc_activity(user_key, user_category_key, user_geographic_region_key,activity_date_key)
VALUES (6, 1, 1,20150515);
Thanks in advance.
The trigger should return NULL
CREATE OR REPLACE FUNCTION insert_function()
RETURNS trigger AS
$BODY$
DECLARE
l_part_date TEXT;
l_table_name TEXT;
BEGIN
l_part_date := to_char(TO_date(NEW.activity_date_key ::text,'YYYYMMDD'),'YYYY_MM');
l_table_name := TG_TABLE_NAME ||'_'|| l_part_date;
EXECUTE 'INSERT INTO ' || l_table_name || ' SELECT $1.*' USING NEW;
RETURN NULL;
END;
$BODY$
LANGUAGE plpgsql;