In my Postgresql db, I keep a journal of all updates against some of my tables.
The journal tables are of the form:
| Column | Type |
| --------------- | ------ |
| journal_id | bigint |
| id | text |
| col_1 | text |
| col_i | any |
journal_id
is an auto increment sequence.- every time the resource is updated, it creates a new record in the journal table for the same
id
and a newjournal_id
I want to create a function show_column_differences
to show the diff between the updates: which columns got updated and the before / after values.
So if the table contains:
| journal_id | id | col_1 | col_i |
| 1 | foo | v1_1 | vi_1 |
| 2 | foo | v1_2 | vi_1 |
| 3 | foo | v1_3 | vi_3 |
It would return
> SELECT * FROM show_column_differences('my_table', 'foo');
| journal_id | previous_journal_id | col_name | value | previous_value |
| 1 | null | col_1 | v1_1 | null |
| 1 | null | col_i | vi_1 | null |
| 2 | 1 | col_1 | v1_2 | v1_1 |
| 3 | 2 | col_1 | v1_3 | v1_2 |
| 3 | 2 | col_i | vi_3 | vi_1 |
I have created a function leveraging the LAG
with OVER (PARTITION BY id ORDER BY journal_id)
but I keep running into issues to get the dynamic list of columns from the record r
:
CREATE OR REPLACE FUNCTION show_column_differences(
p_table_name TEXT,
p_id TEXT
)
RETURNS TABLE (
journal_id TEXT,
column_name TEXT,
old_value TEXT,
new_value TEXT
) AS $$
DECLARE
r RECORD;
col_name TEXT;
old_col_name TEXT;
col_value TEXT;
old_col_value TEXT;
column_list TEXT[];
select_old_columns TEXT := '';
BEGIN
-- Compute the list of columns once
SELECT array_agg(c.column_name::TEXT)
INTO column_list
FROM information_schema.columns c
WHERE c.table_name = p_table_name
-- Build the list of old_journal columns with alias
FOREACH col_name IN ARRAY column_list LOOP
select_old_columns := select_old_columns || format(', old_journal.%I AS old_%I', col_name, col_name);
END LOOP;
-- CTE to compute the lag and join with the old version of each row
FOR r IN EXECUTE format('
WITH journals AS (
SELECT *, lag(journal_id) OVER (PARTITION BY id ORDER BY journal_id ASC) AS old_journal_id
FROM %I
WHERE id = %L
ORDER BY journal_id ASC
)
SELECT
journals.* %s
FROM journals
JOIN journals AS old_journal
ON journals.old_journal_id = old_journal.journal_id
', p_table_name, p_id, select_old_columns)
LOOP
-- Loop over each column and compare old_value and new_value using the precomputed column list
FOREACH col_name IN ARRAY column_list LOOP
old_col_name := 'old_' || col_name;
-- How to access the "col_name" and "old_col_name field of the record?
IF old_value IS DISTINCT FROM new_value THEN
-- Set OUT parameters
-- ...
RETURN NEXT;
END IF;
END LOOP;
END LOOP;
END;
$$ LANGUAGE plpgsql;
- How can I access
r.old_col_name
whereold_col_name
is a variable? - I am open to completely different way to achieve the diff
Thanks for the help