Skip to main content
ertius.org

Postgres change tracking

I've always avoided TRIGGERS and FUNCTIONS inside databases, due to being scar(r?)ed as a wee lad by a huge business app that was mostly implemented in Sybase stored procedures, but today I wanted a lazy history for a simple app. It's a link redirector and one thing that's useful in such a thing is a simple list of changes that have happened for each link, but no real need to mutate it or customise it from the app. I was also feeling pretty lazy, so didn't want to write a bunch of code, so thought I'd try using a trigger function. It was actually extremely easy.

Here's the existing schema:

CREATE TABLE IF NOT EXISTS urls (
       name text PRIMARY KEY,
       url text NOT NULL,
       date_added timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
       creator integer REFERENCES users(id) NOT NULL,
       clicks integer DEFAULT 0 NOT NULL
);

So I added a simple history table for it:

-- History of changes to the urls table.
CREATE TABLE IF NOT EXISTS urls_history (
       id SERIAL,
	   name text NOT NULL,
       tstamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
       "user" integer REFERENCES users(id) NOT NULL,
       "type" text NOT NULL,
       url text NOT NULL,
)

Then I added a function, that handles each case - INSERT/UPDATE/DELETE, just logging the useful info. NEW and OLD are pseudo-tables that let you get the new and old values for the row we're dealing with:

CREATE OR REPLACE FUNCTION change_trigger() RETURNS trigger AS $$
BEGIN
    IF      TG_OP = 'INSERT'
    THEN
        INSERT INTO urls_history ("user", "name", url, type)
            VALUES (NEW.creator, NEW.name, NEW.url, 'ADDED');
            RETURN NEW;
    ELSIF   TG_OP = 'UPDATE'
    THEN
        INSERT INTO urls_history ("user", "name", url, type)
            VALUES (NEW.creator, NEW.name, NEW.url, 'UPDATED');
        RETURN NEW;
    ELSIF   TG_OP = 'DELETE'
    THEN
        INSERT INTO urls_history ("user", "name", url, type)
            VALUES (OLD.creator, OLD.name, OLD.url, 'DELETED');
        RETURN OLD;
    END IF;
END;
$$ LANGUAGE 'plpgsql' SECURITY DEFINER;

Then just strap it on to the data table:

CREATE TRIGGER t
AFTER INSERT OR UPDATE OR DELETE ON urls
        FOR EACH ROW EXECUTE PROCEDURE change_trigger();

And that was it. Then I just need a simple read query in the app and to render the history.