Function trigger with count in postgresql - postgresql

I have created some schemas (dsfv, dsfn, etc.) and insert some tables inside each schema, including the following tables :
poste_hta_bt (parent table with attribute "code_pt" as unique key);
transfo_hta_bt (having also "code_pt" attribute as foreign key that referenced poste_hta_bt).
I have also created a function trigger that count the total number of entities from "transfo_hta_bt" and report it in "nb_transf" attribute of "poste_hta_bt".
My code is as follows :
SET SESSION AUTHORIZATION dsfv;
SET search_path TO dsfv, public;
CREATE TABLE IF NOT EXISTS poste_hta_bt
(
id_pt serial NOT NULL,
code_pt varchar(30) NULL UNIQUE,
etc.
);
CREATE TABLE IF NOT EXISTS transfo_hta_bt
(
id_tra serial NOT NULL,
code_tra varchar(30) NULL,
code_pt varchar(30) NULL,
etc.
);
ALTER TABLE transfo_hta_bt ADD CONSTRAINT "FK_transfo_hta_bt_poste_hta_bt"
FOREIGN KEY (code_pt) REFERENCES poste_hta_bt (code_pt) ON DELETE No Action ON UPDATE No Action;
CREATE OR REPLACE FUNCTION recap_transf() RETURNS TRIGGER
language plpgsql AS
$$
DECLARE
som_transf smallint;
som_transf1 smallint;
BEGIN
IF (TG_OP = 'INSERT') THEN
SELECT COUNT(*) INTO som_transf FROM dsfv.transfo_hta_bt WHERE code_pt = NEW.code_pt;
UPDATE dsfv.poste_hta_bt SET nb_transf = som_transf WHERE dsfv.poste_hta_bt.code_pt = NEW.code_pt;
RETURN NULL;
ELSIF (TG_OP = 'DELETE') THEN
SELECT COUNT(*) INTO som_transf FROM dsfv.transfo_hta_bt WHERE code_pt = OLD.code_pt;
UPDATE dsfv.poste_hta_bt SET nb_transf = som_transf WHERE dsfv.poste_hta_bt.code_pt = OLD.code_pt;
RETURN NULL;
ELSIF (TG_OP = 'UPDATE') THEN
SELECT COUNT(*) INTO som_transf FROM dsfv.transfo_hta_bt WHERE code_pt = NEW.code_pt;
SELECT COUNT(*) INTO som_transf1 FROM dsfv.transfo_hta_bt WHERE code_pt = OLD.code_pt;
UPDATE dsfv.poste_hta_bt SET nb_transf = som_transf WHERE dsfv.poste_hta_bt.code_pt = NEW.code_pt;
UPDATE dsfv.poste_hta_bt SET nb_transf = som_transf1 WHERE dsfv.poste_hta_bt.code_pt = OLD.code_pt;
RETURN NULL;
ELSE
RAISE WARNING 'Other action occurred: %, at %', TG_OP, now();
RETURN NULL;
END IF;
END;
$$
;
DROP TRIGGER IF EXISTS recap_tr ON dsfv.transfo_hta_bt;
CREATE TRIGGER recap_tr AFTER INSERT OR UPDATE OR DELETE ON dsfv.transfo_hta_bt FOR EACH ROW EXECUTE PROCEDURE recap_transf();
This code runs correctly but I didn't understand why the following:
In the function trigger, I noticed that I have to specify the schema of each table, despite that I adjusted search_path to dsfv from the beginning.
Also when I replace dsfv.transfo_hta_bt with TG_TABLE_NAME in the function trigger, the lattest variable is unrecognized.
Thank you in advance for your help.

You need to repeat SET search_path TO dsfv, public; at the beginning of the function body for it to apply to its inner context.
TG_TABLE_NAME is a text variable not unlike TG_OP that you are already using, so you can't directly plug it in a query as a table name. You'd have to construct your queries as text and use dynamic SQL EXECUTE to run them.
CREATE OR REPLACE FUNCTION recap_transf() RETURNS TRIGGER
language plpgsql AS
$$
DECLARE
som_transf smallint;
som_transf1 smallint;
BEGIN
--search path update is rendered somewhat useless by dynamic SQL used later
execute 'SET search_path TO '||TG_TABLE_SCHEMA||', public;';
IF (TG_OP = 'INSERT') THEN
execute format('SELECT COUNT(*) FROM %1$I.%2$I WHERE code_pt = $1',
TG_TABLE_SCHEMA,
TG_TABLE_NAME)
into som_transf
using NEW.code_pt;
execute format('UPDATE %1$I.poste_hta_bt SET nb_transf = $1 WHERE %1$I.poste_hta_bt.code_pt = $2',
TG_TABLE_SCHEMA)
using som_transf,
NEW.code_pt;
RETURN NULL;
ELSIF (TG_OP = 'DELETE') THEN
execute format('SELECT COUNT(*) FROM %1$I.%2$I WHERE code_pt = $1',
TG_TABLE_SCHEMA,
TG_TABLE_NAME)
into som_transf
using OLD.code_pt;
execute format('UPDATE %1$I.poste_hta_bt SET nb_transf = $1 WHERE %1$I.poste_hta_bt.code_pt = $2',
TG_TABLE_SCHEMA)
using som_transf,
OLD.code_pt;
RETURN NULL;
ELSIF (TG_OP = 'UPDATE') THEN
execute format('SELECT COUNT(*) FROM %1$I.%2$I WHERE code_pt = $1',
TG_TABLE_SCHEMA,
TG_TABLE_NAME)
into som_transf
using NEW.code_pt;
execute format('SELECT COUNT(*) FROM %1$I.%2$I WHERE code_pt = $1',
TG_TABLE_SCHEMA,
TG_TABLE_NAME)
into som_transf1
using OLD.code_pt;
execute format('UPDATE %1$I.poste_hta_bt SET nb_transf = $1 WHERE %1$I.poste_hta_bt.code_pt = $2',
TG_TABLE_SCHEMA)
using som_transf,
NEW.code_pt;
execute format('UPDATE %1$I.poste_hta_bt SET nb_transf = $1 WHERE %1$I.poste_hta_bt.code_pt = $2',
TG_TABLE_SCHEMA)
using som_transf1,
OLD.code_pt;
RETURN NULL;
ELSE
RAISE WARNING 'Other action occurred: %, at %', TG_OP, now();
RETURN NULL;
END IF;
END;
$$
;

PostgreSQL stores a function as string, which is interpreted when the function is executed. The search_path that applies is the one in effect when the function is called, not the one in effect when it is created. (SQL functions created with the new syntax from v14 are safe from that, since they are parsed when the function is created.)
To avoid these problems, you should fix the search_path for all functions:
ALTER FUNCTION recap_transf() SET search_path = dsfv;
Note that it is not safe to add a schema where untrusted users can create objects, so only add public if you revoked the CREATE privilege for PUBLIC that it has in versions before v15.

Related

How to create a trigger-function for auditing inside the other function? postgresql

I need to create a function that will create a twin-table for every already existing table in the database. Inside the twin-table there has to be the same columns and their data types with 3 more added for auditing (when was the table modified (time and date), who modified it (user), modification typu (insert, update, delete)). Also for each source table, add a trigger that, after inserting, deleting, or modifying data, adds the corresponding record with new values ​​to the twin table. Iterating over tables requires the use of cursors.
I made the function and it seems to work fine, but the trigger cannot be inserted into it, it complains about incorrect syntax:
CREATE OR REPLACE FUNCTION copy_table() RETURNS boolean language plpgsql as $$
DECLARE
cursor1 CURSOR FOR SELECT table_name from information_schema.tables WHERE table_schema = 'public';
cursor2 CURSOR (key text) FOR SELECT column_name, data_type from information_schema.columns WHERE
table_schema = 'public' and table_name = key ORDER BY ordinal_position;
tablename text;
tablename_new text;
columnname text;
data_type text;
BEGIN
OPEN cursor1;
LOOP
FETCH cursor1 INTO tablename;
tablename_new = tablename || '_copy';
IF NOT FOUND THEN EXIT;
END IF;
EXECUTE 'CREATE TABLE '||tablename_new||' ()';
OPEN cursor2(tablename);
LOOP
FETCH cursor2 INTO columnname, data_type;
IF NOT FOUND THEN EXIT;
END IF;
EXECUTE 'ALTER TABLE '||tablename_new||' ADD '||columnname||' '||data_type||'';
END LOOP;
CLOSE cursor2;
EXECUTE 'ALTER TABLE '||tablename_new||' ADD COLUMN operation text NOT NULL';
EXECUTE 'ALTER TABLE '||tablename_new||' ADD COLUMN stamp timestamp';
EXECUTE 'ALTER TABLE '||tablename_new||' ADD COLUMN userid text NOT NULL';
EXECUTE '
CREATE OR REPLACE FUNCTION audit() RETURNS TRIGGER as
$body1$
BEGIN
IF (TG_OP = 'DELETE') THEN
INSERT INTO public.'||tablename_new||' VALUES (OLD.*, TG_OP, now(), user);
ELSEIF (TG_OP = 'UPDATE') THEN
INSERT INTO public.'||tablename_new||' VALUES (NEW.*, TG_OP, now(), user);
ELSEIF (TG_OP = 'INSERT') THEN
INSERT INTO public.'||tablename_new||' VALUES (NEW.*, TG_OP, now(), user);
END IF;
END;
$body1$
language plpgsql;
CREATE TRIGGER audit_mod_table
AFTER INSERT OR UPDATE OR DELETE ON public.'||tablename||'
FOR EACH ROW EXECUTE FUNCTION audit();';
END LOOP;
CLOSE cursor1;
RETURN TRUE;
END; $$
Complains about the syntax here (specifically ' ' in 'DELETE', 'UPDATE', 'INSERT'), but without them the trigger doesn't work and thinks they are columns:
IF (TG_OP = 'DELETE') THEN
INSERT INTO public.'||tablename_new||' VALUES (OLD.*, TG_OP, now(), user);
ELSEIF (TG_OP = 'UPDATE') THEN
INSERT INTO public.'||tablename_new||' VALUES (NEW.*, TG_OP, now(), user);
ELSEIF (TG_OP = 'INSERT') THEN
INSERT INTO public.'||tablename_new||' VALUES (NEW.*, TG_OP, now(), user);
END IF;

Create Trigger to update a row on same table when insert or update happens using postgresql

I am new to plpgsql and trying the below scenario. It would be helpful if someone could help on this issue.
I have a table named emp_table and whenever an record is inserted, I called a trigger to update a column record_status with 'U' and when insert operation is happened on table, I need to update the record-status column to 'I'
Table :
CREATE TABLE emp_data (
name text,
age integer,
designation text,
salary integer,
last_login TIMESTAMP,
record_status varchar
);
CREATE OR REPLACE FUNCTION em_batch_update()
RETURNS trigger
LANGUAGE PLPGSQL
AS
$$
BEGIN
IF (TG_OP = 'UPDATE') THEN
UPDATE emp_data SET record_status = 'U' WHERE record_status is not null;
RETURN NEW;
ELSIF (TG_OP = 'INSERT') THEN
UPDATE emp_data SET record_status = 'I' WHERE record_status is NULL;
RETURN NEW;
end if;
END;
$$
CREATE TRIGGER em_sem_batch
BEFORE INSERT OR UPDATE ON emp_data
FOR EACH ROW
EXECUTE PROCEDURE em_batch_update();
I have inserted below new record, but the record_status is not getting updated.
insert into emp_data(name,age,designation,salary,last_login) values ('test1234',3,'test1143',1332224,current_timestamp);
I get below error when I either update or insert,
PL/pgSQL function em_batch_update1() line 5 at SQL statement
SQL statement "UPDATE emp_data SET record_status = 'U' WHERE record_status is not null"
PL/pgSQL function em_batch_update1() line 5 at SQL statement
SQL statement "UPDATE emp_data SET record_status = 'I' WHERE record_status is NULL"
PL/pgSQL function em_batch_update1() line 8 at SQL statement
SQL state: 54001
can someone help with this
No need for an UPDATE. In a row-level BEFORE trigger you can simply assign the values:
CREATE OR REPLACE FUNCTION em_batch_update()
RETURNS trigger
LANGUAGE PLPGSQL
AS
$$
BEGIN
IF (TG_OP = 'UPDATE') THEN
new.record_status := 'U';
ELSIF (TG_OP = 'INSERT') THEN
new.record_status := 'I';
end if;
RETURN NEW;
END;
$$
Or even simpler:
CREATE OR REPLACE FUNCTION em_batch_update()
RETURNS trigger
LANGUAGE PLPGSQL
AS
$$
BEGIN
new.record_status := left(TG_OP, 1);
RETURN NEW;
END;
$$

Triggers in Postgres: Access NEW fields by name at runtime

In Postgres, someone knows how to substitute the value of the variable in a NEW.variable in a trigger?
For instance, I have a variable with value order_code. I want to execute NEW.variable so that it's getting in fact NEW.order_code.
In detailed:
I have a function to obtain the primary key column of a table:
CREATE FUNCTION getPrimaryKey(_table_name VARCHAR(50))
RETURNS SETOF VARCHAR(50) AS $$
DECLARE
primary_key VARCHAR(50);
BEGIN
FOR primary_key IN SELECT a.attname
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid
AND a.attnum = ANY(i.indkey)
WHERE i.indrelid = _table_name::regclass
AND i.indisprimary LOOP
RETURN NEXT primary_key;
END LOOP;
END;
$$ LANGUAGE plpgsql;
Then I have a trigger to collect some info when an INSERT is done in a table. The procedure in the trigger is called from several triggers from different tables. That's why it's so generic and I have this need.
What I want is to obtain the primary key of the object inserted.
CREATE FUNCTION logAudit()
RETURNS trigger AS $$
DECLARE primary_key VARCHAR(50);
BEGIN
primary_key := getprimarykey(TG_TABLE_NAME::VARCHAR(50));
INSERT INTO test VALUES (TG_TABLE_NAME);
INSERT INTO test VALUES (NEW.primary_key);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER audit_in_client
AFTER INSERT ON tb_client
FOR EACH STATEMENT EXECUTE PROCEDURE logAudit();
The NEW.primary_key is what is causing me issues. I expect primary_key to be the column name of the source table where the insert happened. What I want in NEW.primary_key is to actually use the value in the variable.
Here is the example of anonymous pl/pgsql block which doing something what you want:
do $$
declare
v pg_database = (pg_database) from pg_database where datname = 'template1';
fname text = 'datname';
n text;
begin
n := to_jsonb(v)->>fname;
raise info '%', n;
end $$;
Output:
INFO: template1
It is working example. In your trigger function it could be something like
declare
pk_name text;
pk_value text;
begin
pk_name := getprimarykey(TG_TABLE_NAME::VARCHAR(50));
pk_value := to_jsonb(NEW) ->> pk_name;
-- Do what you want with pk_value here
return null;
end $$;

Issue with dynamic sql

Hi every one I have multiple spatial tables That I want to control, so that I created a table where I will store the name of the operation applied on my layers tables(insert,update or delete), operation time and the team who did it, number of spatial tables created.
My script table
CREATE TABLE public.monitoring_table
(
operation character(1) COLLATE pg_catalog."default",
operat_ime timestamp without time zone,
userid text COLLATE pg_catalog."default",
dc_team text COLLATE pg_catalog."default",
number_pts_created integer,
id integer NOT NULL DEFAULT nextval('monitoring_table_id_seq'::regclass),
CONSTRAINT monitoring_table_pkey PRIMARY KEY (id)
)
WITH (
OIDS = FALSE
)
TABLESPACE pg_default;
ALTER TABLE public.monitoring_table
OWNER to postgres;
after that I stored all the teams that I have on my table :
insert into monitoring_table (dc_team) values ('abdoulhassan');
insert into monitoring_table (dc_team) values ('abdoulei');
insert into monitoring_table (dc_team) values ('danis');
insert into monitoring_table (dc_team) values ('david');
insert into monitoring_table (dc_team) values ('joseph');
To calculate the number of spatial tables created, I executed this function :
My counting function :
DROP FUNCTION get_dc_team_counting();
CREATE OR REPLACE FUNCTION get_dc_team_counting()
RETURNS bigint AS
$func$
DECLARE
dc_team text;
_tbl_pattern text;
_schema text = 'sige';
_tb_name information_schema.tables.table_name%TYPE;
_tc bigint;
BEGIN
FOR _tb_name IN
SELECT table_name
FROM information_schema.tables
WHERE table_schema = _schema
AND table_name ~ _tbl_pattern
LOOP
EXECUTE format('SELECT count(*) FROM %I.%I where id= 583', _schema, _tb_name)
INTO _tc;
return _tc;
END LOOP;
END
$func$ LANGUAGE plpgsql
To get the team I executed this function :
CREATE OR REPLACE FUNCTION get_team()
RETURNS text AS -- or whatever you want to return
$func$
DECLARE
dc_team text;
_tbl_pattern text;
_schema text = 'public';
_tb_name information_schema.tables.table_name%TYPE; -- currently varchar
_tc text;
BEGIN
FOR _tb_name IN
SELECT table_name
FROM information_schema.tables
WHERE table_schema = _schema
AND table_name ~ _tbl_pattern -- see below!
LOOP
EXECUTE format('SELECT dc_team FROM %I.%I where id = 26', _schema, _tb_name)
INTO _tc;
return _tc;
END LOOP;
END
$func$ LANGUAGE plpgsql;
In my where clause I want to get dynamically the value of the ID. I don't want to give it manually in the function. I don't see how to do it.
Now I created a trigger function to be able to update my table if a row was inserted, updated or deleted. I did it this way :
CREATE OR REPLACE FUNCTION process_monitoring() RETURNS TRIGGER AS $monitoring$
BEGIN
IF (TG_OP = 'DELETE') THEN
update monitoring_table set operation = 'D', operat_ime = now(), userid = user ,dc_team = OLD.dc_team, number_pts_created = get_dc_team_counting() where dc_team = get_team();
RETURN OLD;
ELSIF (TG_OP = 'UPDATE') THEN
update monitoring_table set operation = 'U',operat_ime = now(),userid = user , dc_team = NEW.dc_team, number_pts_created = get_dc_team_counting() where dc_team = get_team();
RETURN NEW;
ELSIF (TG_OP = 'INSERT') THEN
update monitoring_table set operation = 'I', operat_ime = now(), userid = user, dc_team = NEW.dc_team, number_pts_created = get_dc_team_counting() where dc_team = get_team();
RETURN NEW;
END IF;
RETURN NULL;
END;
$monitoring$ LANGUAGE plpgsql;
CREATE TRIGGER monitoring
AFTER INSERT OR UPDATE OR DELETE ON sige.valve
FOR EACH ROW EXECUTE PROCEDURE process_monitoring();
In my previous functions I used known values of ids in the where clauses, but when I try to insert or update a value on the concerned table I get this error :
the control attempted it's end without return
CONTEXT: fonction PL/pgsql get_team()
instruction SQL « update monitoring_table set operation = 'U',operat_ime = now(),userid = user , dc_team = NEW.dc_team, number_pts_created = get_dc_team_counting() where dc_team = get_team() »
fonction PL/pgsql process_monitoring(), ligne 10 à instruction SQL
If you have any idea about the origin of the error and how to get dynamically a value of a field and put it in the where clause tell me please.
I'm a newbie and I'm struggling, any assistance would be warmly appreciated.
I'm using PostgreSQL.

Postgresql dynamic function with current table name

I have a function (audit.create_audit_table()) that accepts an array of table names. It creates a single function audit.if_modified_func() and then loops through each table name and creates an audit table and applies a trigger to the main table. The function compiles and is created with no errors. When I run the function
select audit.create_audit_table(ARRAY['organization'])
I keep getting the following error and I am not sure why because I thought that TG_TABLE_NAME is a automatic variable which will give me access to the current table that is executing audit.if_modified_func()
ERROR:
ERROR: column "tg_table_name" does not exist
LINE 3: audit_row audit.' || quote_ident(TG_TABLE_NAME::TEXT)||';
^
Here's the function:
CREATE OR REPLACE FUNCTION audit.create_audit_table(table_names character varying[])
RETURNS character varying AS
$BODY$
DECLARE
table_name varchar;
i int;
BEGIN
EXECUTE 'CREATE OR REPLACE FUNCTION audit.if_modified_func() RETURNS TRIGGER AS $$
DECLARE
audit_row audit.' || quote_ident(TG_TABLE_NAME::TEXT)||';
include_values boolean;
log_diffs boolean;
h_old hstore;
h_new hstore;
excluded_cols text[] = ARRAY[]::text[];
BEGIN
IF TG_WHEN <> ''AFTER'' THEN
RAISE EXCEPTION ''audit.if_modified_func() may only run as an AFTER trigger'';
END IF;
audit_row = ROW(
nextval(''audit.'|| quote_ident(TG_TABLE_NAME::text) ||'_event_id_seq''), -- event_id
TG_TABLE_SCHEMA::text, -- schema_name
TG_TABLE_NAME::text, -- table_name
TG_RELID, -- relation OID for much quicker searches
session_user::text, -- session_user_name
current_timestamp, -- action_tstamp_tx
statement_timestamp(), -- action_tstamp_stm
clock_timestamp(), -- action_tstamp_clk
txid_current(), -- transaction ID
current_setting(''application_name''), -- client application
inet_client_addr(), -- client_addr
inet_client_port(), -- client_port
current_query(), -- top-level query or queries (if multistatement) from client
substring(TG_OP,1,1), -- action
NULL, NULL, -- row_data, changed_fields
''f'' -- statement_only
);
IF NOT TG_ARGV[0]::boolean IS DISTINCT FROM ''f''::boolean THEN
audit_row.client_query = NULL;
END IF;
IF TG_ARGV[1] IS NOT NULL THEN
excluded_cols = TG_ARGV[1]::text[];
END IF;
IF (TG_OP = ''UPDATE'' AND TG_LEVEL = ''ROW'') THEN
audit_row.row_data = hstore(OLD.*) - excluded_cols;
audit_row.changed_fields = (hstore(NEW.*) - audit_row.row_data) - excluded_cols;
IF audit_row.changed_fields = hstore('''') THEN
-- All changed fields are ignored. Skip this update.
RETURN NULL;
END IF;
ELSIF (TG_OP = ''DELETE'' AND TG_LEVEL = ''ROW'') THEN
audit_row.row_data = hstore(OLD.*) - excluded_cols;
ELSIF (TG_OP = ''INSERT'' AND TG_LEVEL = ''ROW'') THEN
audit_row.row_data = hstore(NEW.*) - excluded_cols;
ELSIF (TG_LEVEL = ''STATEMENT'' AND TG_OP IN (''INSERT'',''UPDATE'',''DELETE'',''TRUNCATE'')) THEN
audit_row.statement_only = ''t'';
ELSE
RAISE EXCEPTION ''[audit.if_modified_func] - Trigger func added as trigger for unhandled case: %%, %%'',TG_OP, TG_LEVEL;
RETURN NULL;
END IF;
INSERT INTO audit.'|| quote_ident(TG_TABLE_NAME::TEXT) ||' VALUES (audit_row.*);
RETURN null;
END;
$$
LANGUAGE plpgsql;
ALTER FUNCTION audit.if_modified_func()
OWNER TO postgres;';
FOR i in 1..array_upper(table_names, 1) LOOP
EXECUTE format('
DROP TABLE IF EXISTS audit.%1$s;
CREATE TABLE audit.%1$s (
event_id bigserial primary key,
schema_name text not null,
table_name text not null,
relid oid not null,
session_user_name text,
action_tstamp_tx TIMESTAMP WITH TIME ZONE NOT NULL,
action_tstamp_stm TIMESTAMP WITH TIME ZONE NOT NULL,
action_tstamp_clk TIMESTAMP WITH TIME ZONE NOT NULL,
transaction_id bigint,
application_name text,
client_addr inet,
client_port integer,
client_query text,
action TEXT NOT NULL CHECK (action IN (''I'',''D'',''U'', ''T'')),
row_data hstore,
changed_fields hstore,
statement_only boolean not null
);
REVOKE ALL ON audit.%1$s FROM public;
COMMENT ON TABLE audit.%1$s IS ''History of auditable actions on audited tables, from audit.if_modified_func()'';
COMMENT ON COLUMN audit.%1$s.event_id IS ''Unique identifier for each auditable event'';
COMMENT ON COLUMN audit.%1$s.schema_name IS ''Database schema audited table for this event is in'';
COMMENT ON COLUMN audit.%1$s.table_name IS ''Non-schema-qualified table name of table event occured in'';
COMMENT ON COLUMN audit.%1$s.relid IS ''Table OID. Changes with drop/create. Get with ''''tablename''''::regclass'';
COMMENT ON COLUMN audit.%1$s.session_user_name IS ''Login / session user whose statement caused the audited event'';
COMMENT ON COLUMN audit.%1$s.action_tstamp_tx IS ''Transaction start timestamp for tx in which audited event occurred'';
COMMENT ON COLUMN audit.%1$s.action_tstamp_stm IS ''Statement start timestamp for tx in which audited event occurred'';
COMMENT ON COLUMN audit.%1$s.action_tstamp_clk IS ''Wall clock time at which audited event''''s trigger call occurred'';
COMMENT ON COLUMN audit.%1$s.transaction_id IS ''Identifier of transaction that made the change. May wrap, but unique paired with action_tstamp_tx.'';
COMMENT ON COLUMN audit.%1$s.client_addr IS ''IP address of client that issued query. Null for unix domain socket.'';
COMMENT ON COLUMN audit.%1$s.client_port IS ''Remote peer IP port address of client that issued query. Undefined for unix socket.'';
COMMENT ON COLUMN audit.%1$s.client_query IS ''Top-level query that caused this auditable event. May be more than one statement.'';
COMMENT ON COLUMN audit.%1$s.application_name IS ''Application name set when this audit event occurred. Can be changed in-session by client.'';
COMMENT ON COLUMN audit.%1$s.action IS ''Action type; I = insert, D = delete, U = update, T = truncate'';
COMMENT ON COLUMN audit.%1$s.row_data IS ''Record value. Null for statement-level trigger. For INSERT this is the new tuple. For DELETE and UPDATE it is the old tuple.'';
COMMENT ON COLUMN audit.%1$s.changed_fields IS ''New values of fields changed by UPDATE. Null except for row-level UPDATE events.'';
COMMENT ON COLUMN audit.%1$s.statement_only IS ''''''t'''' if audit event is from an FOR EACH STATEMENT trigger, ''''f'''' for FOR EACH ROW'';
CREATE INDEX %1$s_relid_idx ON audit.%1$s(relid);
CREATE INDEX %1$s_action_tstamp_tx_stm_idx ON audit.%1$s(action_tstamp_stm);
CREATE INDEX %1$s_action_idx ON audit.%1$s(action);
', table_names[i]);
EXECUTE format('
DROP TRIGGER IF EXISTS audit_trigger_row ON %1$s;
CREATE TRIGGER audit_trigger_row
AFTER INSERT OR UPDATE OR DELETE
ON public.%1$s
FOR EACH ROW
EXECUTE PROCEDURE audit.if_modified_func();', table_names[i]);
EXECUTE format('
DROP TRIGGER IF EXISTS audit_trigger_stm ON %1$s;
CREATE TRIGGER audit_trigger_stm
AFTER TRUNCATE
ON public.%1$s
FOR EACH STATEMENT
EXECUTE PROCEDURE audit.if_modified_func();', table_names[i]);
END LOOP;
RETURN 'SUCCESS';
END;
$BODY$
LANGUAGE plpgsql;
ALTER FUNCTION audit.create_audit_table(character varying[])
OWNER TO postgres;
UPDATE 03/31:
Ok, so I created the if_modified_func() function without the dynamic sql and I declared the audit_row as audit_row RECORD; I am not sure about the part of "needing a cast upon inserting the values". I am also not sure if this is the correct way to do the insert
EXECUTE format($string$INSERT INTO audit.%1$s VALUES (audit_row.*);$string$, TG_TABLE_NAME::text);
I am now getting this error when I run select audit.create_audit_table(ARRAY['organization'])
ERROR:
ERROR: record "audit_row" has no field "row_data"
CONTEXT: PL/pgSQL function audit.if_modified_func() line 42 at assignment
Here's the updated function:
CREATE OR REPLACE FUNCTION audit.if_modified_func() RETURNS TRIGGER AS $$
DECLARE
audit_row RECORD;
include_values boolean;
log_diffs boolean;
h_old hstore;
h_new hstore;
excluded_cols text[] = ARRAY[]::text[];
BEGIN
IF TG_WHEN <> 'AFTER' THEN
RAISE EXCEPTION 'audit.if_modified_func() may only run as an AFTER trigger';
END IF;
audit_row = ROW(
nextval(format('audit.%1$s_event_id_seq',TG_TABLE_NAME::text)), -- event_id
TG_TABLE_SCHEMA::text, -- schema_name
TG_TABLE_NAME::text, -- table_name
TG_RELID, -- relation OID for much quicker searches
session_user::text, -- session_user_name
current_timestamp, -- action_tstamp_tx
statement_timestamp(), -- action_tstamp_stm
clock_timestamp(), -- action_tstamp_clk
txid_current(), -- transaction ID
current_setting('application_name'), -- client application
inet_client_addr(), -- client_addr
inet_client_port(), -- client_port
current_query(), -- top-level query or queries (if multistatement) from client
substring(TG_OP,1,1), -- action
NULL, NULL, -- row_data, changed_fields
'f' -- statement_only
);
IF NOT TG_ARGV[0]::boolean IS DISTINCT FROM 'f'::boolean THEN
audit_row.client_query = NULL;
END IF;
IF TG_ARGV[1] IS NOT NULL THEN
excluded_cols = TG_ARGV[1]::text[];
END IF;
IF (TG_OP = 'UPDATE' AND TG_LEVEL = 'ROW') THEN
audit_row.row_data = hstore(OLD.*) - excluded_cols;
audit_row.changed_fields = (hstore(NEW.*) - audit_row.row_data) - excluded_cols;
IF audit_row.changed_fields = hstore('') THEN
-- All changed fields are ignored. Skip this update.
RETURN NULL;
END IF;
ELSIF (TG_OP = 'DELETE' AND TG_LEVEL = 'ROW') THEN
audit_row.row_data = hstore(OLD.*) - excluded_cols;
ELSIF (TG_OP = 'INSERT' AND TG_LEVEL = 'ROW') THEN
audit_row.row_data = hstore(NEW.*) - excluded_cols;
ELSIF (TG_LEVEL = 'STATEMENT' AND TG_OP IN ('INSERT','UPDATE','DELETE','TRUNCATE')) THEN
audit_row.statement_only = 't';
ELSE
RAISE EXCEPTION '[audit.if_modified_func] - Trigger func added as trigger for unhandled case: %, %',TG_OP, TG_LEVEL;
RETURN NULL;
END IF;
EXECUTE format('INSERT INTO audit.%1$s VALUES (audit_row.*)', TG_TABLE_NAME::text);
RETURN null;
END;
$$
LANGUAGE plpgsql;
ALTER FUNCTION audit.if_modified_func()
OWNER TO postgres;
TG_TABLE_NAME is a special, trigger variable, which is only available inside trigger functions. Your create_audit_table() is not a trigger function.
Also, you constantly redefining your real trigger function (if_modified_func()), which "invalidates" any earlier created triggers.
Create your trigger function without the dynamic SQL magic (dynamic SQL will only need to insert values to these audit tables). Then, you can add your audit logic to a table with:
CREATE TRIGGER audit_trigger_row
AFTER INSERT OR UPDATE OR DELETE
ON public.<your_table_name>
FOR EACH ROW
EXECUTE PROCEDURE <your_audit_trigger_function_name>();
You can put this (but only this -- maybe with drop if exists) inside a function, to allow attaching this audit logic more easily.
Notes:
Inside the trigger function, you cannot use a %ROWTYPE variable (because you don't know the exact table. you only have its name). The solution is simple: just use the RECORD type instead (you will need a cast upon inserting the values though).
Don't use single quotes for such long strings. Use the $your_keyword$<string_value>$your_keyword$ format instead. With possibly the format() function instead of just concatenating values. Your code will be much more readable.
Edit: to utilize your RECORD variable, you should either:
Initialize it with a structure. You can do this in your case with f.ex.
SELECT nextval('audit.'|| quote_ident(TG_TABLE_NAME) || '_event_id_seq') AS event_id,
TG_TABLE_SCHEMA AS schema_name,
TG_TABLE_NAME AS table_name,
TG_RELID AS relid,
session_user AS session_user_name,
current_timestamp AS action_tstamp_tx,
statement_timestamp() AS action_tstamp_stm,
clock_timestamp() AS action_tstamp_clk,
txid_current() AS transaction_id,
current_setting('application_name') AS application_name,
inet_client_addr() AS client_addr,
inet_client_port() AS client_port,
current_query() AS client_query,
substring(TG_OP, 1, 1) AS action,
NULL::hstore AS row_data,
NULL::hstore AS changed_fields,
FALSE AS statement_only
INTO audit_row;
Use the predefined names of the ROW() constructor. The first column's name if f1, the second's is f2, etc.
audit_row.f15 = hstore(OLD.*) - excluded_cols;
After choosing one of the above methods, you should insert the row like:
EXECUTE format('INSERT INTO audit.%1$s VALUES (($1::text::audit.%1$s).*)', quote_ident(TG_TABLE_NAME)) USING audit_row;
Note: even the cast to text is required due to the fact that EXECUTE cannot know the actual structure of audit_row.
http://rextester.com/GUAJ1339
quote_ident(TG_TABLE_NAME::TEXT) will apply necessary actions to correcty quote the argument as relation name.
I would recommend using execute format('statement') instead of concatinations, eg:
t=# do $$ begin raise info '%',format('I am %I, now is %L',current_user,now()); end;$$;
INFO: I am postgres, now is '2017-03-30 07:33:53.579476+00'
DO
Instead of:
t=# do $$ begin raise info '%','I am '||quote_ident(current_user)||', now is '||quote_ident(now()::text); end;$$;
INFO: I am postgres, now is "2017-03-30 07:36:20.495887+00"
DO