Michael Hoennig
2022-10-19 7376301ed44726f483af74ef4bd881463d7ea4a9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
--liquibase formatted sql
 
-- ============================================================================
--changeset audit-OPERATION-TYPE:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
    A type representing a DML operation.
 */
do $$
    begin
        if not exists(select 1 from pg_type where typname = 'operation') then
            create type "operation" as enum ('INSERT', 'UPDATE', 'DELETE', 'TRUNCATE');
        end if;
        --more types here...
    end $$;
--//
 
-- ============================================================================
--changeset audit-TX-CONTEXT-TABLE:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
    A table storing transactions with context data.
 */
create table tx_context
(
    contextId       bigint primary key not null,
    txId            bigint             not null,
    txTimestamp     timestamp          not null,
    currentUser     varchar(63)        not null, -- not the uuid, because users can be deleted
    assumedRoles    varchar            not null, -- not the uuids, because roles can be deleted
    currentTask     varchar(96)        not null,
    currentRequest  varchar(512)       not null
);
 
create index on tx_context using brin (txTimestamp);
--//
 
-- ============================================================================
--changeset audit-TX-JOURNAL-TABLE:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
    A table storing the transaction audit journal for all target tables it's configured for.
 */
create table tx_journal
(
    contextId   bigint    not null references tx_context (contextId),
    targetTable text      not null,
    targetUuid  uuid      not null, -- Assumes that all audited tables have a uuid column.
    targetOp    operation not null,
    targetDelta jsonb
);
 
create index on tx_journal (targetTable, targetUuid);
--//
 
-- ============================================================================
--changeset audit-TX-JOURNAL-TRIGGER:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
    Trigger function for transaction audit journal.
 */
create or replace function tx_journal_trigger()
    returns trigger
    language plpgsql as $$
declare
    curTask text;
    curContextId bigint;
begin
    curTask := currentTask();
    curContextId := txid_current()+bigIntHash(curTask);
 
    insert
        into tx_context (contextId, txId, txTimestamp, currentUser, assumedRoles, currentTask, currentRequest)
        values (curContextId, txid_current(), now(),
                currentUser(), assumedRoles(), curTask, currentRequest())
        on conflict do nothing;
 
    case tg_op
        when 'INSERT' then insert
                               into tx_journal
                               values (curContextId,
                                       tg_table_name, new.uuid, tg_op::operation,
                                       to_jsonb(new));
        when 'UPDATE' then insert
                               into tx_journal
                               values (curContextId,
                                       tg_table_name, old.uuid, tg_op::operation,
                                       jsonb_changes_delta(to_jsonb(old), to_jsonb(new)));
        when 'DELETE' then insert
                               into tx_journal
                               values (curContextId,
                                       tg_table_name, old.uuid, 'DELETE'::operation,
                                       null::jsonb);
        else raise exception 'Trigger op % not supported for %.', tg_op, tg_table_name;
        end case;
    return null;
end; $$;
--//
 
-- ============================================================================
--changeset audit-CREATE-JOURNAL-LOG:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
    Trigger function for transaction audit journal.
 */
 
create or replace procedure create_journal(targetTable varchar)
    language plpgsql as $$
declare
    createTriggerSQL varchar;
begin
    targetTable := lower(targetTable);
 
    createTriggerSQL = 'CREATE TRIGGER ' || targetTable || '_journal' ||
                       ' AFTER INSERT OR UPDATE OR DELETE ON ' || targetTable ||
                       '   FOR EACH ROW EXECUTE PROCEDURE tx_journal_trigger()';
    execute createTriggerSQL;
end; $$;
--//