Custom Temporal Tables in PostgreSQL

The following script will add support for temporal tables in PostgreSQL.

Description

It will add two new functions in the public schema:

1) function temporal_table_trigger

Trigger function that inserts temporal data.

This function will create a new schema (source schema plus __history) and a new table with temporal data in it.

The new table will have the exact same schema without keys and constraints with one extra field - data_valid_to timestamptz not null default now();

2) function init_temporal_tables

This function initiates the entire temporal tables system by creating trigger temporal_table_trigger on each tables It accepts an array of schema names we want to have temporal tables.

To call this function for the public schema:

select init_temporal_tables(array['public']);
select init_temporal_tables(array['public']);

Script

create or replace function temporal_table_trigger()
returns trigger
language plpgsql
as
$$
declare
    _schema text;
    _table text;
begin   
    _schema = TG_TABLE_SCHEMA::text || '__history';
    _table = TG_TABLE_NAME::text;
    if not exists(  
            select 1 from information_schema.tables 
            where table_schema = _schema and table_name = _table
    ) then
        execute format(
            $sql$
            create schema if not exists %1$I;
            create table %1$I.%2$I as
            select * from %3$I.%4$I limit 0;
            alter table %1$I.%2$I add column data_valid_to timestamptz not null default now();
            $sql$, 
            _schema, _table, TG_TABLE_SCHEMA::text, TG_TABLE_NAME::text
        );
    end if;
    
    execute format(
        $sql$
        insert into %1$I.%2$I
        select $1.*;
        $sql$, 
        _schema, _table
    ) using old;
        
    return old;
end;
$$;

create or replace function init_temporal_tables(
    _schemas text[],
    _dry_run boolean = false
)
returns void
language plpgsql
as
$$
declare
    _schema text;
    _table text;
    _cmd text;
    _trigger text = 'temporal_table_trigger';
begin   
    for _schema, _table in (
        select tbl.table_schema, tbl.table_name 
        from 
            information_schema.tables tbl
            left join information_schema.triggers tr 
            on tbl.table_schema = tr.event_object_schema 
                and tbl.table_name = tr.event_object_table
                and tr.trigger_name = _trigger
        where 
            tr.event_object_table is null 
            and table_schema = any(_schemas) 
            and table_type = 'BASE TABLE'
    )
    loop
        
        _cmd = format(
            $sql$
            create or replace trigger %1$I 
            after delete or update on %2$I.%3$I 
            for each row execute procedure %1$I();
            $sql$, 
            _trigger, _schema, _table
        );
        
        raise info '%', replace(_cmd, '    ', '');
        
        if _dry_run is false then
            execute _cmd;
        end if;
        
    end loop;
end;
$$;
create or replace function temporal_table_trigger()
returns trigger
language plpgsql
as
$$
declare
    _schema text;
    _table text;
begin   
    _schema = TG_TABLE_SCHEMA::text || '__history';
    _table = TG_TABLE_NAME::text;
    if not exists(  
            select 1 from information_schema.tables 
            where table_schema = _schema and table_name = _table
    ) then
        execute format(
            $sql$
            create schema if not exists %1$I;
            create table %1$I.%2$I as
            select * from %3$I.%4$I limit 0;
            alter table %1$I.%2$I add column data_valid_to timestamptz not null default now();
            $sql$, 
            _schema, _table, TG_TABLE_SCHEMA::text, TG_TABLE_NAME::text
        );
    end if;
    
    execute format(
        $sql$
        insert into %1$I.%2$I
        select $1.*;
        $sql$, 
        _schema, _table
    ) using old;
        
    return old;
end;
$$;

create or replace function init_temporal_tables(
    _schemas text[],
    _dry_run boolean = false
)
returns void
language plpgsql
as
$$
declare
    _schema text;
    _table text;
    _cmd text;
    _trigger text = 'temporal_table_trigger';
begin   
    for _schema, _table in (
        select tbl.table_schema, tbl.table_name 
        from 
            information_schema.tables tbl
            left join information_schema.triggers tr 
            on tbl.table_schema = tr.event_object_schema 
                and tbl.table_name = tr.event_object_table
                and tr.trigger_name = _trigger
        where 
            tr.event_object_table is null 
            and table_schema = any(_schemas) 
            and table_type = 'BASE TABLE'
    )
    loop
        
        _cmd = format(
            $sql$
            create or replace trigger %1$I 
            after delete or update on %2$I.%3$I 
            for each row execute procedure %1$I();
            $sql$, 
            _trigger, _schema, _table
        );
        
        raise info '%', replace(_cmd, '    ', '');
        
        if _dry_run is false then
            execute _cmd;
        end if;
        
    end loop;
end;
$$;
To receive notifications about new posts and updates, consider subscribing to my LinkdIn page:
vb-software linkedin

You will receive notifications about new posts on your LinkedIn feed.
Comments
If you like my content, use my software, or otherwise benefit from my work, consider supporting me by buying me a coffee. The software runs on coffee after all.
Buy me a Coffee Scan Here To Buy Me a Cofee