#PostgreSQL 10.22
Lets say I have a table and a view of it such as in:
CREATE TABLE item
(
id integer Not Null,
name varchar(50) Not Null
);
CREATE OR REPLACE VIEW all_items AS(
SELECT i.id
FROM item i
)
WITH CHECK OPTION
When I try to insert a tuple to all_items view I get an error because of the 'Not Null' constraint on the base table. Eg:
INSERT INTO all_items
VALUES (999)
ERROR: null value in column "name" violates not-null constraint
DETAIL: Failing row contains (999, null).
I tried to use triggers in order for it to work but it didn't:
CREATE OR REPLACE FUNCTION fill_NULL_attributes()
RETURNS trigger AS $$
BEGIN
IF NEW.name IS NULL THEN
NEW.name := 'X'; -- fills empty attribute with some value
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql
CREATE TRIGGER all_items_insert_fix
INSTEAD OF INSERT ON all_items
FOR EACH ROW
EXECUTE PROCEDURE fill_NULL_attributes();
This trigger solution didn't work because NEW doesn't have the "name" attribute.
Is there any way of doing this?
Your trigger function would be correct in a BEFORE trigger, since it modifies the row you are about to insert. But that is not correct in an INSTEAD OF trigger: there, you have to perform the INSERT into the base table yourself.
I used this example and it worked well when all my tables were in public schema.
But trigger hasn't been working since I separated tables into different schemas and applied inheriting.
Here is example of my structure:
CREATE SCHEMA common;
CREATE SCHEMA video;
CREATE TABLE common.file (
file_id SERIAL PRIMARY KEY,
url VARCHAR(255) NOT NULL,
mime_type VARCHAR(31) DEFAULT '' NOT NULL,
size INTEGER NOT NULL,
modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
CREATE TABLE video.file (
width INTEGER NOT NULL,
height INTEGER NOT NULL,
local_path VARCHAR(255) DEFAULT '' NOT NULL
)
INHERITS (common.file);
CREATE FUNCTION common.update_modified()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.modified = now();
RETURN NEW;
END;
CREATE TRIGGER update_modified
BEFORE UPDATE ON common.file
FOR EACH ROW EXECUTE PROCEDURE common.update_modified();
When I do UPDATE common.file ... or UPDATE video.file ... field common.file.modified doesn't change itself. It seems trigger doesn't run, but I don't understand why.
What should I do to repair the behavior?
In described issue trigger is set only on common.file, so UPDATE common.file ... doesn't work if row inserted in video.file
Documentation says: INSERT always inserts into exactly the table specified
So trigger should be applied to both common.file and video.file.
-- Creating schemas and tables the same
-- Let function be in public scheme
CREATE FUNCTION update_modified()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.modified = now();
RETURN NEW;
END;
CREATE TRIGGER update_modified
BEFORE UPDATE ON common.file
FOR EACH ROW EXECUTE PROCEDURE update_modified();
CREATE TRIGGER update_modified
BEFORE UPDATE ON video.file
FOR EACH ROW EXECUTE PROCEDURE update_modified();
In that case when we update rows inserted either in common.file or in video.file corresponding trigger will call.
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
Good morning everyone, I have a question about the following case:
I have a trigger and a function that inserts a land code, but when it works very well when inserting a row.
But when an insert statement fails to execute for any problems in the expression, the sequence function generates a value before inserting the row, losing the order in the numeration.
There is a way to make a change in the trigger or function, to validate me before the INSERT expression before moving to the sequence function and thereby avoid those jumps of numeration.
Deputy code (triger and function) and images of the tables.
CODE:
CREATE TRIGGER trigger_codigo_pech
BEFORE INSERT ON independizacion
FOR EACH ROW
EXECUTE PROCEDURE codigo_pech();
CREATE OR REPLACE FUNCTION codigo_pech()
RETURNS trigger
AS $$
DECLARE
incremento INTEGER;
cod_inde text;
BEGIN
IF (NEW.cod_inde IS NULL OR NEW.cod_inde = '''' ) THEN
incremento = nextval ('codigo_pech');
NEW.cod_inde = 'PECH' || '-' || incremento;
END IF;
RETURN NEW;
END;
$$ LANGUAGE 'plpgsql';
CAPTURE QUERY RESULT
As you can see, it would also be necessary to make a trigger on the primary key to prevent jumps in the numeration.
I hope your help. Thank you
You can make incremento.cod_inde DEFERRABLE and INITIALLY DEFERRED:
ALTER TABLE incremento ALTER COLUMN cod_inde SET DEFAULT 0;
ALTER TABLE incremento
ALTER CONSTRAINT incremento_cod_inde_key
DEFERRABLE INITIALLY DEFERRED;
Then assign the nextval('codigo_pech') in a AFTER INSERT trigger:
CREATE OR REPLACE FUNCTION codigo_pech_after() RETURNS trigger AS $$
BEGIN
UPDATE incremento SET
cod_inde = 'PECH-' || (nextval('codigo_pech'))::text
WHERE id = NEW.id; -- replace id with your table's primary key
END;
$$ LANGUAGE plpgsql;
I have a trigger function that copy row of unique values to another table on update or insert that ALMOST work.
The trigger should only insert a new row to the sample table if the number don't exist in it before. Atm. it insert a new row to the sample table with the value NULL if the number already exist in the table. I dont want it to do anything if maintbl.number = sample.nb_main
EDIT: sample table and sample data
CREATE TABLE schema.main(
sid SERIAL NOT NULL,
number INTEGER,
CONSTRAINT sid_pk PRIMARY KEY (sid)
)
CREATE TABLE schema.sample(
gid SERIAL NOT NULL,
nb_main INTEGER,
CONSTRAINT gid_pk PRIMARY KEY (gid)
Example and desired result
schema.main schema.sample
number nb_main
234233 234233
234234 555555
234234
555555
555555
CREATE OR REPLACE FUNCTION schema.update_number()
RETURNS trigger AS
$BODY$
BEGIN
INSERT INTO schema.sample(
nb_main)
SELECT DISTINCT(maintbl.number)
FROM schema.maintbl
WHERE NOT EXISTS (
SELECT nb_main FROM schema.sample WHERE maintbl.number = sample.nb_main);
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION schema.update_number()
OWNER TO postgres;
CREATE TRIGGER update_number
AFTER INSERT OR UPDATE
ON schema.maintbl
FOR EACH ROW
EXECUTE PROCEDURE schema.update_number();
I just found out that my select query is probably wrong, if I run SELECT query by itself it return one row 'NULL' but i should not?
SELECT DISTINCT(maintbl.number)
FROM schema.maintbl
WHERE NOT EXISTS (
SELECT nb_main FROM schema.sample WHERE maintbl.number = sample.nb_main);
Any good advice?
Best
If I understood correctly, you wish to append to schema.sample a number that has been inserted or updated in schema.maintbl, right?
CREATE OR REPLACE FUNCTION schema.update_number()
RETURNS trigger AS
$BODY$
BEGIN
IF (SELECT COUNT(*) FROM schema.sample WHERE number = NEW.number) = 0 THEN
INSERT INTO schema.sample(nb_main) VALUES (NEW.number);
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;