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

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.

Related

Dynamic column name postgresql trigger

I'm trying to create a dynamic audit trigger that reads a column_name from the information schema and inserts into an audit table which column has been changed. So far I have the following code:
CREATE OR REPLACE FUNCTION dynamic_column_audit()
RETURNS trigger
LANGUAGE 'plpgsql'
COST 100
VOLATILE NOT LEAKPROOF AS $BODY$
<<main_function>>
declare
table_name_tr text;
table_schema_tr text;
column_name_trigger text;
old_column_name_trigger record;
new_column_name_trigger record;
begin
-- variables
table_name_tr := TG_TABLE_NAME;
table_schema_tr := TG_TABLE_SCHEMA;
<<loop_sql>>
FOR column_name_trigger IN (SELECT c.column_name FROM information_schema.columns c WHERE c.table_schema = table_schema_tr and c.table_name = table_name_tr ORDER BY ordinal_position asc)
LOOP
RAISE INFO E'\n Column name: %, table_name: %', column_name_trigger , table_schema_tr||'.'||table_name_tr;
IF (TG_OP = 'UPDATE') THEN
INSERT INTO my_loggingtable (operation, table_schema, table_name, column_name, old_value, new_value)
VALUES (TG_OP, table_name_tr, table_schema_tr, column_name_trigger, OLD.||column_name_trigger, NEW.||column_name_trigger);
end if;
END LOOP loop_sql;
return new;
end main_function;
$BODY$;
I'm trying to see whether it's possible to do something like NEW. + column_name or OLD> + column_name from the loop rather than hardcode the column name value for each table.
The idea behind this, which it could not be possible is to do an audit trigger for around 20 tables without adding the name all the time.
You can use a construct like this:
DECLARE
newval text;
BEGIN
EXECUTE format(
'SELECT ($1::%I).%I',
table_name,
column_name
)
INTO newval
USING NEW;
END;

Oracle procedure/function to create a trigger in table

I'm trying to create a procedure that given a table name, it will create a sequence and auto incrementing trigger, all using variables based on the table name.
Code :
CREATE OR REPLACE procedure CREATE_SEQUENTIAL_TR(table_name VARCHAR)
is -- Tried using declare but it wouldn't accept
coluna_cod varchar(100 char);
begin
--Finding the cod column name for this table first
--They start with "PK_CD"
select
COLUMN_NAME
into
coluna_cod
from
ALL_TAB_COLUMNS
where
TABLE_NAME=table_name
and COLUMN_NAME like "PK_CD%";
--Creating the sequence obj
drop sequence "cod" || table_name;
create sequence "cod" || table_name;
--Now creating the trigger
create or replace trigger "cod" || table_name || "tr"
before
UPDATE or INSERT on table_name
for each row
declare
cod number := coluna_cod;
tr_name varchar(100 char) := "cod" || table_name
begin
if UPDATING then
if :new.cod != :old.cod then
:new.cod := :old.cod;
end if;
else -- inserting
:new.cod := tr_name.nextval();
end if;
end;
end;
The complexity of this ended up quite out of the scope of my knowledge.
At the moment it is giving an error on drop sequence "cod" || table_name (Unexpected DROP symbol found) but I'm sure I have made other errors.
Can someone help me figure this logic out?
You can't put DDL statements (like drop or create or alter) directly inside a PL/SQL block. If you want to do DDL inside PL/SQL, you can do an execute immediate:
declare
begin
drop sequence X; -- error
execute immediate 'drop sequence X'; -- works fine
end;
/

PostgreSQL: How to add a column in every table of a database?

I have a database with 169 tables
I need this column in every table:
wid integer not null primary key
I tried this(Thanks https://stackoverflow.com/users/27535/gbn for the solution):
SELECT
'ALTER TABLE ' + T.name + ' ADD foo int NULL'
FROM
sys.tables AS T
WHERE
T.is_ms_shipped = 0
But it didn't work on PostgreSQL.
It only worked on tsql.
How to add this column in every table at once ?
do $$
declare
selectrow record;
begin
for selectrow in
select
'ALTER TABLE '|| T.mytable || ' ADD COLUMN foo integer NULL' as script
from
(
select tablename as mytable from pg_tables where schemaname ='public' --your schema name here
) t
loop
execute selectrow.script;
end loop;
end;
$$;
You can test whether all your tables altered with the new column using the following select
select
table_name,COLUMN_NAME
from
INFORMATION_SCHEMA.COLUMNS
where
COLUMN_NAME='foo' -- column name here
Try this (change 'public' to whatever schema you're doing this in)
DO $$
DECLARE
row record;
cmd text;
BEGIN
FOR row IN SELECT schemaname, tablename FROM pg_tables WHERE schemaname = 'public' LOOP
cmd := format('ALTER TABLE %I.%I ADD COLUMN foo SERIAL PRIMARY KEY ', row.schemaname, row.tablename);
RAISE NOTICE '%', cmd;
-- EXECUTE cmd;
END LOOP;
END
$$ LANGUAGE plpgsql;
If you run as is, it'll show you the commands. Uncomment the EXECUTE line to actually perform the alterations.
I'd run within a transaction so you can roll back if you're not happy with the results.
Note that the type is SERIAL - the column type will be integer, but also creates a sequence owned by the table and defaults the column value to the next value of that sequence.
We may need to check column is already exist or not.
Tested on PostgreSQL V10
do $$
declare selectrow record;
begin
for selectrow in
select 'ALTER TABLE '|| T.mytable || ' ADD COLUMN x_is_exported boolean DEFAULT FALSE' as script
from (select tablename as mytable from pg_tables where schemaname ='public') t
loop
begin
execute selectrow.script;
EXCEPTION WHEN duplicate_column THEN CONTINUE;
END;
end loop;
end;
$$;

Trigger on every update or insert

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

pl/pgsql: How show values of NEW & OLD trigger record if i don't know the name of original table colum?

Hi all,
I wrote a function in pl/pgsql and I have this problem:
I want use the values of NEW and OLD trigger record but I don't know the column name and number if the table.
for example:
CREATE OR REPLACE FUNCTION tt() RETURNS trigger AS $$
DECLARE
text1 text;
text2 text;
orig_name text =tg_argv[0];
orig_schema text =tg_argv[1];
log_name text =tg_argv[2];
log_schema text =tg_argv[3];
col pg_attribute.attname%TYPE;
[...]
BEGIN
orig_comp := quote_ident(orig_schema)||'.'||quote_ident(orig_name);
log_comp := quote_ident(log_schema)||'.'||quote_ident(log_name);
IF(trigger_mode='INSERT')
THEN
-- I want know the names of column
FOR colonna in
SELECT attname
FROM pg_attribute
WHERE attrelid = orig_comp::regclass
AND attnum > 0
AND NOT attisdropped
loop --for each column I want save the value like a string
text1=NEW||'.'||colonna; -- this don't work: error: record NEW don't have colonna like values
text2:=text2||' , '||text1;
end loop;
[...]
END IF;
[...]
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
you know a original name - it is a variable TG_TABLE_NAME. And dynamic access to record fields is possible with EXECUTE USING statement.
CREATE OR REPLACE FUNCTION dynamic_trigger()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
ri RECORD;
t TEXT;
BEGIN
RAISE NOTICE E'\n Operation: %\n Schema: %\n Table: %',
TG_OP,
TG_TABLE_SCHEMA,
TG_TABLE_NAME;
FOR ri IN
SELECT ordinal_position, column_name, data_type
FROM information_schema.columns
WHERE
table_schema = quote_ident(TG_TABLE_SCHEMA)
AND table_name = quote_ident(TG_TABLE_NAME)
ORDER BY ordinal_position
LOOP
EXECUTE 'SELECT ($1).' || ri.column_name || '::text' INTO t USING NEW;
RAISE NOTICE E'Column\n number: %\n name: %\n type: %\n value: %.',
ri.ordinal_position,
ri.column_name,
ri.data_type,
t;
END LOOP;
RETURN NEW;
END; $$;
This code is wrote by Tom Lane and it is from postgresql tricks pages Iteration over RECORD variable inside trigger.