Generic INSERT/UPDATE/DELETE logging on PostgreSQL

home

Description

The following code is a PostgreSQL PL/Perl database trigger. It logs any changes to any database which which it is bound at to a table in a dedicated table schema (tested with PostgreSQL 8.3.7):

--
-- This trigger functions can be bound on any table. It creates a new table
-- history.<SCHEMA>_<TABLENAME> with two additional columns:
--
--  hist_ts timestamp DEFAULT NOW()
--  hist_op character(6)
--
-- "hist_ts" is the timestamp when the change has been occured. "hist_op"
-- is the DB operation which has caused the trigger (INSERT, UPDATE, DELETE).
--
-- The code tries to use some caching, when the original table
-- has been changed, ensure you change the log table accordently
-- and kill any running sessions.
--
-- 2009/08/06 - Thomas Liske <liske@ibh.de>
--
CREATE OR REPLACE FUNCTION log_history() RETURNS trigger
    AS $$

    my $htblname = $_TD->{table_schema}.'_'.$_TD->{table_name};

    unless(defined($_SHARED{'pl_log_history_texists'})) {
        elog(DEBUG, "Preparing 'pl_log_history_texists'.");

        $_SHARED{'pl_log_history_texists'} =
         spi_prepare("SELECT schemaname FROM pg_tables WHERE schemaname='history' AND tablename=\$1", 'text')
    }

    my $sth = spi_query_prepared($_SHARED{'pl_log_history_texists'}, $htblname);
    unless(defined(spi_fetchrow($sth))) {
        elog(NOTICE, "Creating history table for '$_TD->{table_schema}.$_TD->{table_name}'.");

        spi_exec_query("CREATE TABLE history.$htblname (LIKE $_TD->{table_schema}.$_TD->{table_name});");
        spi_exec_query("ALTER TABLE history.$htblname ADD COLUMN hist_ts timestamp DEFAULT NOW();");
        spi_exec_query("ALTER TABLE history.$htblname ADD COLUMN hist_op character(6);");
    }

    my $use = 'new';
    $use = 'old' if($_TD->{event} eq 'DELETE');

    my $plshared = "pl_log_history_insert#$_TD->{table_schema}.$_TD->{table_name}";
    unless(defined($_SHARED{$plshared})) {
        elog(NOTICE, "Preparing '$plshared'.");

        $rv = spi_exec_query('SELECT a.attname,pg_catalog.format_type(a.atttypid,a.atttypmod) AS atttype FROM '.
                             'pg_catalog.pg_attribute a WHERE '.
                             'a.attnum>0 AND NOT a.attisdropped AND a.attrelid=('.
                             ' SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN'.
                             ' pg_catalog.pg_namespace n ON n.oid = c.relnamespace'.
                             " WHERE c.relname = '$_TD->{table_name}' AND n.nspname = '$_TD->{table_schema}' AND".
                             ' pg_catalog.pg_table_is_visible(c.oid));');

        my @colnames;
        my @colparams;
        my @coltypes;
        my $i = 1;
        foreach my $row (@{$rv->{rows}}) {
            push(@colnames, $row->{attname});
            push(@colparams, '$'.$i);
            push(@coltypes, $row->{atttype});
            $i++;
        }
        push(@colnames, 'hist_op');
        push(@colparams, '$'.$i);
        push(@coltypes, 'character(6)');
        my $query = "INSERT INTO history.$htblname (\"";
        $query .= join('","', @colnames);
        $query .= '") VALUES (';
        $query .= join(',', @colparams);
        $query .= ');';

        $_SHARED{$plshared} = spi_prepare($query, @coltypes);
        $_SHARED{$plshared."#names"} = \@colnames;
    }

    if(defined($_SHARED{$plshared})) {
        $_TD->{$use}{hist_op} = $_TD->{event};
        my @cols;
        foreach my $col (@{$_SHARED{$plshared."#names"}}) {
            push(@cols, $_TD->{$use}{$col});
        }

        spi_exec_prepared($_SHARED{$plshared}, @cols);
    }
    else {
        elog(WARNING, 'Could not log history due plan is undefined!');
    }

    return;
$$
    LANGUAGE plperl;

To bind the trigger on all tables in a schema you might use the following function:

CREATE OR REPLACE FUNCTION history_create_triggers(schema text) RETURNS void
    AS $$
    DECLARE
        r RECORD;
    BEGIN
        FOR r IN SELECT table_name FROM information_schema.tables WHERE table_schema = schema LOOP
            EXECUTE 'CREATE TRIGGER log_history AFTER INSERT OR UPDATE OR DELETE ON ' || schema || '.' || r.table_name || ' FOR EACH ROW EXECUTE PROCEDURE log_history();';
        END LOOP;
    END;
$$
    LANGUAGE plpgsql;

Just call the function for your schema like: SELECT history_create_triggers('public');