NOTE: The following example is a strongly simplified code, but it illustrates the task in a reproducible example. Looking at this example may make you think "why do it like this"? In reality, the full task is to store an audit-trail for multiple tables, but only if certain conditions are met. The condition is the same for each table (they each share some columns like inserted
, updated
and so on). So the code when to store an audit-trail is the same for each table. But the actual columns to be copied are different each time. I weant to create a trigger which handles this dynamically such that I don't need to touch it each time the schema changes.
Consider the following working example (question below). This demonstrates a simple schema where every update in table data
causes the old values to be moved into data2
:
DROP TABLE IF EXISTS data CASCADE;
DROP TABLE IF EXISTS data2 CASCADE;
CREATE TABLE data (
id SERIAL,
name TEXT,
updated TIMESTAMP WITH TIME ZONE
);
CREATE TABLE data2 (
id SERIAL,
name TEXT,
updated TIMESTAMP WITH TIME ZONE
);
CREATE OR REPLACE FUNCTION update_trigger_func()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated = NOW();
INSERT INTO data2 VALUES (OLD.*);
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_trigger
BEFORE UPDATE ON data
FOR EACH ROW
EXECUTE PROCEDURE update_trigger_func();
SET client_min_messages TO 'debug';
INSERT INTO data (name) VALUES ('foo');
COMMIT; -- Make sure we get new timestamps from NOW()
SELECT * FROM ONLY data;
SELECT * FROM ONLY data2;
SELECT pg_sleep(1);
UPDATE data SET name = 'bar';
COMMIT; -- Make sure we get new timestamps from NOW()
SELECT * FROM ONLY data;
SELECT * FROM ONLY data2;
SELECT pg_sleep(1);
UPDATE data SET name = 'baz';
COMMIT; -- Make sure we get new timestamps from NOW()
SELECT * FROM ONLY data;
SELECT * FROM ONLY data2;
Notice that the function update_trigger_func
has the "history" table name hard-coded as data2
on the line which reads:
INSERT INTO data2 VALUES (OLD.*);
If data2
was an argument, this function could be reusable for other tables as well. But so far I failed to find the right incantation. I've tried the following two versions so far:
INSERT INTO TG_ARGV[0] VALUES (OLD.*);
But this causes a syntax error:
psql:temptable.sql:28: ERROR: syntax error at or near "VALUES"
LINE 11: INSERT INTO TG_ARGV[0] VALUES (OLD.*);
So alternatively I tried with dynamic SQL:
sql := 'INSERT INTO' || TG_ARGV[0] || 'VALUES (OLD.*)';
EXECUTE sql;
But that fails because the OLD
variable is not available in the execution context:
psql:temptable.sql:58: ERROR: missing FROM-clause entry for table "old"
LINE 1: INSERT INTO data2 VALUES (OLD.*)
^
QUERY: INSERT INTO data2 VALUES (OLD.*)
CONTEXT: PL/pgSQL function versioned_update() line 11 at EXECUTE
Given that I would like to use this trigger-function on other tables I can't hard-code the column names. How could I achieve this?
Best Answer
Unfortunately, googling this was fairly difficult as the data-type of
OLD
isdata
, and searching for this in Google turned up nothing usable. Additionally, finding out thatdata
is a composite type was key to finding the solution.The issue is twofold:
The table cannot directly be used in an
INSERT
statement as it is of typetext
. So dynamic SQL has to be used. So instead of directly writingthe following had to be used:
The
OLD
variable is not available directly inside anEXECUTE
statement. So, building from the previous bullet-point, this was not possible:instead the values from
OLD
had to be passed on with theUSING
keyword:The final solution:
This allows us to write a trigger which handles events on the
data
table and writes the history intohistory
table as: