pax_global_header00006660000000000000000000000064144612436470014525gustar00rootroot0000000000000052 comment=85929cf2bca9a65f4d2630b26cc93508aa3c2b59 table_log-0.6.4/000077500000000000000000000000001446124364700134645ustar00rootroot00000000000000table_log-0.6.4/.github/000077500000000000000000000000001446124364700150245ustar00rootroot00000000000000table_log-0.6.4/.github/workflows/000077500000000000000000000000001446124364700170615ustar00rootroot00000000000000table_log-0.6.4/.github/workflows/regression.yml000066400000000000000000000010661446124364700217670ustar00rootroot00000000000000name: make installcheck on: [push, pull_request] jobs: test: strategy: matrix: pg: - 16 - 15 - 14 - 13 - 12 - 11 - 10 - 9.6 name: PostgreSQL ${{ matrix.pg }} runs-on: ubuntu-latest container: pgxn/pgxn-tools steps: - name: Start PostgreSQL ${{ matrix.pg }} run: pg-start ${{ matrix.pg }} - name: Check out the repo uses: actions/checkout@v2 - name: Test on PostgreSQL ${{ matrix.pg }} run: pg-build-test table_log-0.6.4/.gitignore000066400000000000000000000002531446124364700154540ustar00rootroot00000000000000# http://www.gnu.org/software/automake Makefile.in # http://www.gnu.org/software/autoconf /autom4te.cache /aclocal.m4 /compile /configure /depcomp /install-sh /missing table_log-0.6.4/Makefile000066400000000000000000000006071446124364700151270ustar00rootroot00000000000000MODULES = table_log EXTENSION = table_log DATA = table_log--0.6.1.sql table_log--unpackaged--0.6.1.sql table_log--0.5--0.6.1.sql table_log--0.6--0.6.1.sql ## keep it for non-EXTENSION installations ## DATA_built = table_log.sql uninstall_table_log.sql DOCS = table_log.md REGRESS = table_log ifndef PG_CONFIG PG_CONFIG = pg_config endif PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) table_log-0.6.4/README.md000077700000000000000000000000001446124364700172102table_log.mdustar00rootroot00000000000000table_log-0.6.4/debian/000077500000000000000000000000001446124364700147065ustar00rootroot00000000000000table_log-0.6.4/debian/changelog000066400000000000000000000014011446124364700165540ustar00rootroot00000000000000tablelog (0.6.4-1) unstable; urgency=medium * Fix compatibility with PG16. -- Christoph Berg Sat, 29 Jul 2023 19:04:19 +0200 tablelog (0.6.3-2) unstable; urgency=medium * Upload for PostgreSQL 15. -- Christoph Berg Wed, 09 Nov 2022 13:53:50 +0100 tablelog (0.6.3-1) unstable; urgency=medium * Fix error in table_log_restore_table. -- Christoph Berg Thu, 20 Jan 2022 16:10:34 +0100 tablelog (0.6.2-1) unstable; urgency=medium * New upstream version. * Modernize packaging. -- Christoph Berg Thu, 20 Jan 2022 14:11:22 +0100 tablelog (0.6.1-1) unstable; urgency=low * Initial release. -- Christoph Berg Tue, 19 Nov 2013 14:43:32 +0100 table_log-0.6.4/debian/control000066400000000000000000000014271446124364700163150ustar00rootroot00000000000000Source: tablelog Section: database Priority: optional Maintainer: Debian PostgreSQL Maintainers Uploaders: Christoph Berg Build-Depends: debhelper-compat (= 13), postgresql-all (>= 217~) Standards-Version: 4.6.0 Rules-Requires-Root: no Homepage: https://github.com/df7cb/table_log Vcs-Git: https://github.com/df7cb/table_log.git Vcs-Browser: https://github.com/df7cb/table_log Package: postgresql-15-tablelog Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, ${postgresql:Depends} Description: log changes on tables and restore tables to point in time table_log is a PostgreSQL extension with a set of functions to log changes on a table and to restore the state of the table or a specific row on any time in the past. table_log-0.6.4/debian/control.in000066400000000000000000000014361446124364700167220ustar00rootroot00000000000000Source: tablelog Section: database Priority: optional Maintainer: Debian PostgreSQL Maintainers Uploaders: Christoph Berg Build-Depends: debhelper-compat (= 13), postgresql-all (>= 217~) Standards-Version: 4.6.0 Rules-Requires-Root: no Homepage: https://github.com/df7cb/table_log Vcs-Git: https://github.com/df7cb/table_log.git Vcs-Browser: https://github.com/df7cb/table_log Package: postgresql-PGVERSION-tablelog Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, ${postgresql:Depends} Description: log changes on tables and restore tables to point in time table_log is a PostgreSQL extension with a set of functions to log changes on a table and to restore the state of the table or a specific row on any time in the past. table_log-0.6.4/debian/copyright000066400000000000000000000017431446124364700166460ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: table_log Source: https://github.com/df7cb/table_log Files: * Copyright: (c) 2002-2007 Andreas Scherbaum License: ads Files: debian/* Copyright: 2013, 2022 Christoph Berg License: ads License: ads Basically it's the same as the PostgreSQL license (BSD license). . Permission to use, copy, modify, and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of the author not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. The author makes no representations about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty. table_log-0.6.4/debian/docs000066400000000000000000000000121446124364700155520ustar00rootroot00000000000000README.md table_log-0.6.4/debian/pgversions000066400000000000000000000000041446124364700170220ustar00rootroot00000000000000all table_log-0.6.4/debian/rules000077500000000000000000000000521446124364700157630ustar00rootroot00000000000000#!/usr/bin/make -f %: dh $@ --with pgxs table_log-0.6.4/debian/source/000077500000000000000000000000001446124364700162065ustar00rootroot00000000000000table_log-0.6.4/debian/source/format000066400000000000000000000000141446124364700174140ustar00rootroot000000000000003.0 (quilt) table_log-0.6.4/debian/tests/000077500000000000000000000000001446124364700160505ustar00rootroot00000000000000table_log-0.6.4/debian/tests/control000066400000000000000000000001131446124364700174460ustar00rootroot00000000000000Depends: @, make Tests: installcheck Restrictions: needs-root allow-stderr table_log-0.6.4/debian/tests/installcheck000077500000000000000000000000541446124364700204410ustar00rootroot00000000000000#!/bin/sh set -e pg_buildext installcheck table_log-0.6.4/debian/watch000066400000000000000000000001021446124364700157300ustar00rootroot00000000000000version=4 https://github.com/df7cb/table_log/tags .*/v(.*).tar.gz table_log-0.6.4/expected/000077500000000000000000000000001446124364700152655ustar00rootroot00000000000000table_log-0.6.4/expected/table_log.out000066400000000000000000000326341446124364700177560ustar00rootroot00000000000000CREATE EXTENSION table_log; SET client_min_messages TO warning; -- drop old trigger DROP TRIGGER test_log_chg ON test; -- ignore any error ERROR: relation "test" does not exist -- create demo table DROP TABLE test; -- ignore any error ERROR: table "test" does not exist CREATE TABLE test ( id INT NOT NULL PRIMARY KEY, name VARCHAR(20) NOT NULL ); -- create the table without data from demo table DROP TABLE test_log; -- ignore any error ERROR: table "test_log" does not exist SELECT * INTO test_log FROM test LIMIT 0; ALTER TABLE test_log ADD COLUMN trigger_mode VARCHAR(10); ALTER TABLE test_log ADD COLUMN trigger_tuple VARCHAR(5); ALTER TABLE test_log ADD COLUMN trigger_changed TIMESTAMPTZ; ALTER TABLE test_log ADD COLUMN trigger_id BIGINT; CREATE SEQUENCE test_log_id; SELECT SETVAL('test_log_id', 1, FALSE); setval -------- 1 (1 row) ALTER TABLE test_log ALTER COLUMN trigger_id SET DEFAULT NEXTVAL('test_log_id'); -- create trigger CREATE TRIGGER test_log_chg AFTER UPDATE OR INSERT OR DELETE ON test FOR EACH ROW EXECUTE PROCEDURE table_log(); -- test trigger INSERT INTO test VALUES (1, 'name'); SELECT id, name FROM test; id | name ----+------ 1 | name (1 row) SELECT id, name, trigger_mode, trigger_tuple, trigger_id FROM test_log; id | name | trigger_mode | trigger_tuple | trigger_id ----+------+--------------+---------------+------------ 1 | name | INSERT | new | 1 (1 row) UPDATE test SET name='other name' WHERE id=1; SELECT id, name FROM test; id | name ----+------------ 1 | other name (1 row) SELECT id, name, trigger_mode, trigger_tuple, trigger_id FROM test_log; id | name | trigger_mode | trigger_tuple | trigger_id ----+------------+--------------+---------------+------------ 1 | name | INSERT | new | 1 1 | name | UPDATE | old | 2 1 | other name | UPDATE | new | 3 (3 rows) -- create restore table SELECT table_log_restore_table('test', 'id', 'test_log', 'trigger_id', 'test_recover', NOW()); table_log_restore_table ------------------------- test_recover (1 row) SELECT id, name FROM test_recover; id | name ----+------------ 1 | other name (1 row) DROP TABLE test; DROP TABLE test_log; DROP TABLE test_recover; -- test table_log_init with all arguments -- trigger_user and trigger_changed might differ, so ignore it SET client_min_messages TO warning; CREATE TABLE test(id integer, name text); SELECT table_log_init(5, 'test'); table_log_init ---------------- (1 row) INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); INSERT INTO test VALUES(4, 'Jeanne D''Arc'); UPDATE test SET name = 'veronica' WHERE id = 3; UPDATE test SET name = 'Jeanne D''Arc updated' WHERE id = 4; DELETE FROM test WHERE id = 1; SELECT id, name, trigger_mode, trigger_tuple, trigger_id FROM test_log; id | name | trigger_mode | trigger_tuple | trigger_id ----+----------------------+--------------+---------------+------------ 1 | joe | INSERT | new | 1 2 | barney | INSERT | new | 2 3 | monica | INSERT | new | 3 4 | Jeanne D'Arc | INSERT | new | 4 3 | monica | UPDATE | old | 5 3 | veronica | UPDATE | new | 6 4 | Jeanne D'Arc | UPDATE | old | 7 4 | Jeanne D'Arc updated | UPDATE | new | 8 1 | joe | DELETE | old | 9 (9 rows) DROP TABLE test; DROP TABLE test_log; DROP SEQUENCE test_log_seq; CREATE TABLE test(id integer, name text); SELECT table_log_init(4, 'test'); table_log_init ---------------- (1 row) INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); INSERT INTO test VALUES(4, 'Jeanne D''Arc'); UPDATE test SET name = 'veronica' WHERE id = 3; UPDATE test SET name = 'Jeanne D''Arc updated' WHERE id = 4; DELETE FROM test WHERE id = 1; SELECT id, name, trigger_mode, trigger_tuple, trigger_id FROM test_log; id | name | trigger_mode | trigger_tuple | trigger_id ----+----------------------+--------------+---------------+------------ 1 | joe | INSERT | new | 1 2 | barney | INSERT | new | 2 3 | monica | INSERT | new | 3 4 | Jeanne D'Arc | INSERT | new | 4 3 | monica | UPDATE | old | 5 3 | veronica | UPDATE | new | 6 4 | Jeanne D'Arc | UPDATE | old | 7 4 | Jeanne D'Arc updated | UPDATE | new | 8 1 | joe | DELETE | old | 9 (9 rows) DROP TABLE test; DROP TABLE test_log; DROP SEQUENCE test_log_seq; CREATE TABLE test(id integer, name text); SELECT table_log_init(3, 'test'); table_log_init ---------------- (1 row) INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); INSERT INTO test VALUES(4, 'Jeanne D''Arc'); UPDATE test SET name = 'veronica' WHERE id = 3; UPDATE test SET name = 'Jeanne D''Arc updated' WHERE id = 4; DELETE FROM test WHERE id = 1; SELECT id, name, trigger_mode, trigger_tuple FROM test_log; id | name | trigger_mode | trigger_tuple ----+----------------------+--------------+--------------- 1 | joe | INSERT | new 2 | barney | INSERT | new 3 | monica | INSERT | new 4 | Jeanne D'Arc | INSERT | new 3 | monica | UPDATE | old 3 | veronica | UPDATE | new 4 | Jeanne D'Arc | UPDATE | old 4 | Jeanne D'Arc updated | UPDATE | new 1 | joe | DELETE | old (9 rows) DROP TABLE test; DROP TABLE test_log; -- Check table_log_restore_table() CREATE TABLE test(id integer, name text); ALTER TABLE test ADD PRIMARY KEY(id); SELECT table_log_init(5, 'test'); table_log_init ---------------- (1 row) INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); INSERT INTO test VALUES(4, 'Jeanne D''Arc'); UPDATE test SET name = 'veronica' WHERE id = 3; UPDATE test SET name = 'Jeanne D''Arc updated' WHERE id = 4; DELETE FROM test WHERE id = 1; SELECT table_log_restore_table('test', 'id', 'test_log', 'trigger_id', 'test_recover', NOW(), '2', NULL::int, 1); table_log_restore_table ------------------------- test_recover (1 row) SELECT id, name FROM test_recover; id | name ----+-------- 2 | barney (1 row) DROP TABLE test; DROP TABLE test_log; DROP TABLE test_recover; DROP SEQUENCE test_log_seq; -- Check partition support with auto-generated -- log table name. CREATE TABLE test(id integer, name text); ALTER TABLE test ADD PRIMARY KEY(id); SELECT table_log_init(5, 'public', 'test', 'public', NULL, 'PARTITION'); table_log_init ---------------- (1 row) SET table_log.active_partition = 0; INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); SET table_log.active_partition = 1; UPDATE test SET name = 'veronica' WHERE id = 3; DELETE FROM test WHERE id = 1; SELECT id, name FROM test_log; id | name ----+---------- 1 | joe 2 | barney 3 | monica 3 | monica 3 | veronica 1 | joe (6 rows) SELECT id, name FROM test_log_0; id | name ----+-------- 1 | joe 2 | barney 3 | monica (3 rows) SELECT id, name FROM test_log_1; id | name ----+---------- 3 | monica 3 | veronica 1 | joe (3 rows) -- -- NOTE: since we use partitioning here, we use the special log -- view to restore the data with id = 2 -- SELECT table_log_restore_table('test', 'id', 'test_log', 'trigger_id', 'test_recover', NOW(), '3', NULL::int, 1); table_log_restore_table ------------------------- test_recover (1 row) SELECT id, name FROM test_recover; id | name ----+---------- 3 | veronica (1 row) DROP TABLE test; DROP VIEW test_log; DROP TABLE test_log_0; DROP TABLE test_log_1; DROP TABLE test_recover; DROP SEQUENCE test_log_seq; -- -- Check with schema-qualified restore and log table -- CREATE SCHEMA log; CREATE TABLE test(id integer, name text); ALTER TABLE test ADD PRIMARY KEY(id); SELECT table_log_init(5, 'public', 'test', 'log', NULL, 'PARTITION'); table_log_init ---------------- (1 row) SET table_log.active_partition = 0; INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); SET table_log.active_partition = 1; UPDATE test SET name = 'veronica' WHERE id = 3; SELECT * FROM test; id | name ----+---------- 1 | joe 2 | barney 3 | veronica (3 rows) DELETE FROM test WHERE id = 1; SELECT * FROM test; id | name ----+---------- 2 | barney 3 | veronica (2 rows) SELECT id, name FROM log.test_log; id | name ----+---------- 1 | joe 2 | barney 3 | monica 3 | monica 3 | veronica 1 | joe (6 rows) SELECT id, name FROM log.test_log_0; id | name ----+-------- 1 | joe 2 | barney 3 | monica (3 rows) SELECT id, name FROM log.test_log_1; id | name ----+---------- 3 | monica 3 | veronica 1 | joe (3 rows) -- -- NOTE: since we use partitioning here, we use the special log -- view to restore the data with id = 2 -- SELECT table_log_restore_table('test', 'id', 'log.test_log', 'trigger_id', 'log.test_recover', NOW(), '3', NULL::int, 1); table_log_restore_table ------------------------- log.test_recover (1 row) SELECT id, name FROM log.test_recover; id | name ----+---------- 3 | veronica (1 row) DROP SCHEMA log CASCADE; DROP TABLE test; -- -- Test with a schema-qualified and case sensitive log and -- restore table -- CREATE SCHEMA log; CREATE TABLE test(id integer, name text); ALTER TABLE test ADD PRIMARY KEY(id); SELECT table_log_init(5, 'public', 'test', 'log', 'Test', 'PARTITION'); table_log_init ---------------- (1 row) -- Check for log partitions SELECT 'log."Test_0"'::regclass; regclass -------------- log."Test_0" (1 row) SELECT 'log."Test_1"'::regclass; regclass -------------- log."Test_1" (1 row) SET table_log.active_partition = 0; INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); SET table_log.active_partition = 1; UPDATE test SET name = 'veronica' WHERE id = 3; SELECT * FROM test; id | name ----+---------- 1 | joe 2 | barney 3 | veronica (3 rows) DELETE FROM test WHERE id = 1; SELECT * FROM test; id | name ----+---------- 2 | barney 3 | veronica (2 rows) SELECT id, name FROM log."Test"; id | name ----+---------- 1 | joe 2 | barney 3 | monica 3 | monica 3 | veronica 1 | joe (6 rows) SELECT id, name FROM log."Test_0"; id | name ----+-------- 1 | joe 2 | barney 3 | monica (3 rows) SELECT id, name FROM log."Test_1"; id | name ----+---------- 3 | monica 3 | veronica 1 | joe (3 rows) -- -- NOTE: since we use partitioning here, we use the special log -- view to restore the data with id = 2 -- SELECT table_log_restore_table('test', 'id', 'log."Test"', 'trigger_id', 'log."Test_recover"', NOW(), '3', NULL::int, 1); table_log_restore_table ------------------------- log."Test_recover" (1 row) SELECT id, name FROM log."Test_recover"; id | name ----+---------- 3 | veronica (1 row) DROP SCHEMA log CASCADE; DROP TABLE test; -- -- Test basic log mode -- CREATE SCHEMA log; CREATE TABLE test(id integer, name text); ALTER TABLE test ADD PRIMARY KEY(id); -- this should fail, no trigger actions specified SELECT table_log_init(5, 'public', 'test', 'log', NULL, 'PARTITION', true, '{}'); ERROR: table_log_init: at least one trigger action must be specified -- this should succeed, but leave out inserts -- generate the log table this time... SELECT table_log_init(5, 'public', 'test', 'log', NULL, 'PARTITION', true, '{UPDATE, DELETE}'); table_log_init ---------------- (1 row) -- Log partitions created? SELECT 'log.test_log_0'::regclass; regclass ---------------- log.test_log_0 (1 row) SELECT 'log.test_log_1'::regclass; regclass ---------------- log.test_log_1 (1 row) SET table_log.active_partition = 0; INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); SET table_log.active_partition = 1; UPDATE test SET name = 'veronica' WHERE id = 3; SELECT * FROM test; id | name ----+---------- 1 | joe 2 | barney 3 | veronica (3 rows) DELETE FROM test WHERE id = 1; SELECT * FROM test; id | name ----+---------- 2 | barney 3 | veronica (2 rows) -- UPDATE logged only, but only old tuples SELECT id, name FROM log.test_log; id | name ----+-------- 3 | monica 1 | joe (2 rows) SELECT id, name FROM log.test_log_0; id | name ----+------ (0 rows) SELECT id, name FROM log.test_log_1; id | name ----+-------- 3 | monica 1 | joe (2 rows) -- NOTE: -- We don't test table_log_restore_table() at this point, since -- restore from data collected by table_log_basic() is not yet supported. DROP SCHEMA log CASCADE; DROP TABLE test; RESET client_min_messages; table_log-0.6.4/expected/table_log_1.out000066400000000000000000000330111446124364700201640ustar00rootroot00000000000000CREATE EXTENSION table_log; SET client_min_messages TO warning; -- drop old trigger DROP TRIGGER test_log_chg ON test; -- ignore any error ERROR: relation "test" does not exist -- create demo table DROP TABLE test; -- ignore any error ERROR: table "test" does not exist CREATE TABLE test ( id INT NOT NULL PRIMARY KEY, name VARCHAR(20) NOT NULL ); -- create the table without data from demo table DROP TABLE test_log; -- ignore any error ERROR: table "test_log" does not exist SELECT * INTO test_log FROM test LIMIT 0; ALTER TABLE test_log ADD COLUMN trigger_mode VARCHAR(10); ALTER TABLE test_log ADD COLUMN trigger_tuple VARCHAR(5); ALTER TABLE test_log ADD COLUMN trigger_changed TIMESTAMPTZ; ALTER TABLE test_log ADD COLUMN trigger_id BIGINT; CREATE SEQUENCE test_log_id; SELECT SETVAL('test_log_id', 1, FALSE); setval -------- 1 (1 row) ALTER TABLE test_log ALTER COLUMN trigger_id SET DEFAULT NEXTVAL('test_log_id'); -- create trigger CREATE TRIGGER test_log_chg AFTER UPDATE OR INSERT OR DELETE ON test FOR EACH ROW EXECUTE PROCEDURE table_log(); -- test trigger INSERT INTO test VALUES (1, 'name'); SELECT id, name FROM test; id | name ----+------ 1 | name (1 row) SELECT id, name, trigger_mode, trigger_tuple, trigger_id FROM test_log; id | name | trigger_mode | trigger_tuple | trigger_id ----+------+--------------+---------------+------------ 1 | name | INSERT | new | 1 (1 row) UPDATE test SET name='other name' WHERE id=1; SELECT id, name FROM test; id | name ----+------------ 1 | other name (1 row) SELECT id, name, trigger_mode, trigger_tuple, trigger_id FROM test_log; id | name | trigger_mode | trigger_tuple | trigger_id ----+------------+--------------+---------------+------------ 1 | name | INSERT | new | 1 1 | name | UPDATE | old | 2 1 | other name | UPDATE | new | 3 (3 rows) -- create restore table SELECT table_log_restore_table('test', 'id', 'test_log', 'trigger_id', 'test_recover', NOW()); table_log_restore_table ------------------------- test_recover (1 row) SELECT id, name FROM test_recover; id | name ----+------------ 1 | other name (1 row) DROP TABLE test; DROP TABLE test_log; DROP TABLE test_recover; -- test table_log_init with all arguments -- trigger_user and trigger_changed might differ, so ignore it SET client_min_messages TO warning; CREATE TABLE test(id integer, name text); SELECT table_log_init(5, 'test'); table_log_init ---------------- (1 row) INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); INSERT INTO test VALUES(4, 'Jeanne D''Arc'); UPDATE test SET name = 'veronica' WHERE id = 3; UPDATE test SET name = 'Jeanne D''Arc updated' WHERE id = 4; DELETE FROM test WHERE id = 1; SELECT id, name, trigger_mode, trigger_tuple, trigger_id FROM test_log; id | name | trigger_mode | trigger_tuple | trigger_id ----+----------------------+--------------+---------------+------------ 1 | joe | INSERT | new | 1 2 | barney | INSERT | new | 2 3 | monica | INSERT | new | 3 4 | Jeanne D'Arc | INSERT | new | 4 3 | monica | UPDATE | old | 5 3 | veronica | UPDATE | new | 6 4 | Jeanne D'Arc | UPDATE | old | 7 4 | Jeanne D'Arc updated | UPDATE | new | 8 1 | joe | DELETE | old | 9 (9 rows) DROP TABLE test; DROP TABLE test_log; DROP SEQUENCE test_log_seq; CREATE TABLE test(id integer, name text); SELECT table_log_init(4, 'test'); table_log_init ---------------- (1 row) INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); INSERT INTO test VALUES(4, 'Jeanne D''Arc'); UPDATE test SET name = 'veronica' WHERE id = 3; UPDATE test SET name = 'Jeanne D''Arc updated' WHERE id = 4; DELETE FROM test WHERE id = 1; SELECT id, name, trigger_mode, trigger_tuple, trigger_id FROM test_log; id | name | trigger_mode | trigger_tuple | trigger_id ----+----------------------+--------------+---------------+------------ 1 | joe | INSERT | new | 1 2 | barney | INSERT | new | 2 3 | monica | INSERT | new | 3 4 | Jeanne D'Arc | INSERT | new | 4 3 | monica | UPDATE | old | 5 3 | veronica | UPDATE | new | 6 4 | Jeanne D'Arc | UPDATE | old | 7 4 | Jeanne D'Arc updated | UPDATE | new | 8 1 | joe | DELETE | old | 9 (9 rows) DROP TABLE test; DROP TABLE test_log; DROP SEQUENCE test_log_seq; CREATE TABLE test(id integer, name text); SELECT table_log_init(3, 'test'); table_log_init ---------------- (1 row) INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); INSERT INTO test VALUES(4, 'Jeanne D''Arc'); UPDATE test SET name = 'veronica' WHERE id = 3; UPDATE test SET name = 'Jeanne D''Arc updated' WHERE id = 4; DELETE FROM test WHERE id = 1; SELECT id, name, trigger_mode, trigger_tuple FROM test_log; id | name | trigger_mode | trigger_tuple ----+----------------------+--------------+--------------- 1 | joe | INSERT | new 2 | barney | INSERT | new 3 | monica | INSERT | new 4 | Jeanne D'Arc | INSERT | new 3 | monica | UPDATE | old 3 | veronica | UPDATE | new 4 | Jeanne D'Arc | UPDATE | old 4 | Jeanne D'Arc updated | UPDATE | new 1 | joe | DELETE | old (9 rows) DROP TABLE test; DROP TABLE test_log; -- Check table_log_restore_table() CREATE TABLE test(id integer, name text); ALTER TABLE test ADD PRIMARY KEY(id); SELECT table_log_init(5, 'test'); table_log_init ---------------- (1 row) INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); INSERT INTO test VALUES(4, 'Jeanne D''Arc'); UPDATE test SET name = 'veronica' WHERE id = 3; UPDATE test SET name = 'Jeanne D''Arc updated' WHERE id = 4; DELETE FROM test WHERE id = 1; SELECT table_log_restore_table('test', 'id', 'test_log', 'trigger_id', 'test_recover', NOW(), '2', NULL::int, 1); table_log_restore_table ------------------------- test_recover (1 row) SELECT id, name FROM test_recover; id | name ----+-------- 2 | barney (1 row) DROP TABLE test; DROP TABLE test_log; DROP TABLE test_recover; DROP SEQUENCE test_log_seq; -- Check partition support with auto-generated -- log table name. CREATE TABLE test(id integer, name text); ALTER TABLE test ADD PRIMARY KEY(id); SELECT table_log_init(5, 'public', 'test', 'public', NULL, 'PARTITION'); table_log_init ---------------- (1 row) SET table_log.active_partition = 0; INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); SET table_log.active_partition = 1; UPDATE test SET name = 'veronica' WHERE id = 3; DELETE FROM test WHERE id = 1; SELECT id, name FROM test_log; id | name ----+---------- 1 | joe 2 | barney 3 | monica 3 | monica 3 | veronica 1 | joe (6 rows) SELECT id, name FROM test_log_0; id | name ----+-------- 1 | joe 2 | barney 3 | monica (3 rows) SELECT id, name FROM test_log_1; id | name ----+---------- 3 | monica 3 | veronica 1 | joe (3 rows) -- -- NOTE: since we use partitioning here, we use the special log -- view to restore the data with id = 2 -- SELECT table_log_restore_table('test', 'id', 'test_log', 'trigger_id', 'test_recover', NOW(), '3', NULL::int, 1); table_log_restore_table ------------------------- test_recover (1 row) SELECT id, name FROM test_recover; id | name ----+---------- 3 | veronica (1 row) DROP TABLE test; DROP VIEW test_log; DROP TABLE test_log_0; DROP TABLE test_log_1; DROP TABLE test_recover; DROP SEQUENCE test_log_seq; -- -- Check with schema-qualified restore and log table -- CREATE SCHEMA log; CREATE TABLE test(id integer, name text); ALTER TABLE test ADD PRIMARY KEY(id); SELECT table_log_init(5, 'public', 'test', 'log', NULL, 'PARTITION'); table_log_init ---------------- (1 row) SET table_log.active_partition = 0; INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); SET table_log.active_partition = 1; UPDATE test SET name = 'veronica' WHERE id = 3; SELECT * FROM test; id | name ----+---------- 1 | joe 2 | barney 3 | veronica (3 rows) DELETE FROM test WHERE id = 1; SELECT * FROM test; id | name ----+---------- 2 | barney 3 | veronica (2 rows) SELECT id, name FROM log.test_log; id | name ----+---------- 1 | joe 2 | barney 3 | monica 3 | monica 3 | veronica 1 | joe (6 rows) SELECT id, name FROM log.test_log_0; id | name ----+-------- 1 | joe 2 | barney 3 | monica (3 rows) SELECT id, name FROM log.test_log_1; id | name ----+---------- 3 | monica 3 | veronica 1 | joe (3 rows) -- -- NOTE: since we use partitioning here, we use the special log -- view to restore the data with id = 2 -- SELECT table_log_restore_table('test', 'id', 'log.test_log', 'trigger_id', 'log.test_recover', NOW(), '3', NULL::int, 1); table_log_restore_table ------------------------- log.test_recover (1 row) SELECT id, name FROM log.test_recover; id | name ----+---------- 3 | veronica (1 row) DROP SCHEMA log CASCADE; DROP TABLE test; -- -- Test with a schema-qualified and case sensitive log and -- restore table -- CREATE SCHEMA log; CREATE TABLE test(id integer, name text); ALTER TABLE test ADD PRIMARY KEY(id); SELECT table_log_init(5, 'public', 'test', 'log', 'Test', 'PARTITION'); table_log_init ---------------- (1 row) -- Check for log partitions SELECT 'log."Test_0"'::regclass; regclass -------------- log."Test_0" (1 row) SELECT 'log."Test_1"'::regclass; regclass -------------- log."Test_1" (1 row) SET table_log.active_partition = 0; INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); SET table_log.active_partition = 1; UPDATE test SET name = 'veronica' WHERE id = 3; SELECT * FROM test; id | name ----+---------- 1 | joe 2 | barney 3 | veronica (3 rows) DELETE FROM test WHERE id = 1; SELECT * FROM test; id | name ----+---------- 2 | barney 3 | veronica (2 rows) SELECT id, name FROM log."Test"; id | name ----+---------- 1 | joe 2 | barney 3 | monica 3 | monica 3 | veronica 1 | joe (6 rows) SELECT id, name FROM log."Test_0"; id | name ----+-------- 1 | joe 2 | barney 3 | monica (3 rows) SELECT id, name FROM log."Test_1"; id | name ----+---------- 3 | monica 3 | veronica 1 | joe (3 rows) -- -- NOTE: since we use partitioning here, we use the special log -- view to restore the data with id = 2 -- SELECT table_log_restore_table('test', 'id', 'log."Test"', 'trigger_id', 'log."Test_recover"', NOW(), '3', NULL::int, 1); table_log_restore_table ------------------------- log."Test_recover" (1 row) SELECT id, name FROM log."Test_recover"; id | name ----+---------- 3 | veronica (1 row) DROP SCHEMA log CASCADE; DROP TABLE test; -- -- Test basic log mode -- CREATE SCHEMA log; CREATE TABLE test(id integer, name text); ALTER TABLE test ADD PRIMARY KEY(id); -- this should fail, no trigger actions specified SELECT table_log_init(5, 'public', 'test', 'log', NULL, 'PARTITION', true, '{}'); ERROR: table_log_init: at least one trigger action must be specified CONTEXT: PL/pgSQL function table_log_init(integer,text,text,text,text,text,boolean,text[]) line 34 at RAISE -- this should succeed, but leave out inserts -- generate the log table this time... SELECT table_log_init(5, 'public', 'test', 'log', NULL, 'PARTITION', true, '{UPDATE, DELETE}'); table_log_init ---------------- (1 row) -- Log partitions created? SELECT 'log.test_log_0'::regclass; regclass ---------------- log.test_log_0 (1 row) SELECT 'log.test_log_1'::regclass; regclass ---------------- log.test_log_1 (1 row) SET table_log.active_partition = 0; INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); SET table_log.active_partition = 1; UPDATE test SET name = 'veronica' WHERE id = 3; SELECT * FROM test; id | name ----+---------- 1 | joe 2 | barney 3 | veronica (3 rows) DELETE FROM test WHERE id = 1; SELECT * FROM test; id | name ----+---------- 2 | barney 3 | veronica (2 rows) -- UPDATE logged only, but only old tuples SELECT id, name FROM log.test_log; id | name ----+-------- 3 | monica 1 | joe (2 rows) SELECT id, name FROM log.test_log_0; id | name ----+------ (0 rows) SELECT id, name FROM log.test_log_1; id | name ----+-------- 3 | monica 1 | joe (2 rows) -- NOTE: -- We don't test table_log_restore_table() at this point, since -- restore from data collected by table_log_basic() is not yet supported. DROP SCHEMA log CASCADE; DROP TABLE test; RESET client_min_messages; table_log-0.6.4/rpm/000077500000000000000000000000001446124364700142625ustar00rootroot00000000000000table_log-0.6.4/rpm/table_log.spec000066400000000000000000000032561446124364700170740ustar00rootroot00000000000000# rpmbuild -D'pgmajorversion 14' -D'pginstdir /usr/pgsql-14' -ba rpm/table_log.spec %global debug_package %{nil} %global sname table_log %if 0%{?rhel} && 0%{?rhel} == 7 %ifarch ppc64 ppc64le %pgdg_set_ppc64le_compiler_at10 %endif %endif Summary: PostgreSQL table log extension Name: %{sname}_%{pgmajorversion} Version: 0.6.3 Release: 1%{?dist} License: BSD Source0: https://github.com/df7cb/table_log/archive/v%{version}.tar.gz URL: https://github.com/df7cb/table_log/ BuildRequires: postgresql%{pgmajorversion}-devel pgdg-srpm-macros Requires: postgresql%{pgmajorversion}-server %if 0%{?rhel} && 0%{?rhel} == 7 %ifarch ppc64 ppc64le %pgdg_set_ppc64le_min_requires %endif %endif %description table_log is a PostgreSQL extension with a set of functions to log changes on a table and to restore the state of the table or a specific row on any time in the past. %prep %setup -q -n table_log-%{version} %build %if 0%{?rhel} && 0%{?rhel} == 7 %ifarch ppc64 ppc64le %pgdg_set_ppc64le_compiler_flags %endif %endif USE_PGXS=1 PATH=%{pginstdir}/bin/:$PATH %{__make} %{?_smp_mflags} %install %{__rm} -rf %{buildroot} USE_PGXS=1 PATH=%{pginstdir}/bin/:$PATH %{__make} DESTDIR=%{buildroot} %{?_smp_mflags} install %{__install} -d %{buildroot}%{pginstdir}/share/extension %{__install} -d %{buildroot}%{pginstdir}/bin %clean %{__rm} -rf %{buildroot} %files %defattr(644,root,root,755) %doc %{pginstdir}/doc/extension/table_log.md %{pginstdir}/lib/table_log* %{pginstdir}/lib/bitcode/table_log* %{pginstdir}/share/extension/table_log*.sql* %{pginstdir}/share/extension/table_log.control %changelog * Thu Jan 20 2022 Christoph Berg - 0.6.3-1 - Initial packaging for PostgreSQL RPM Repository table_log-0.6.4/sql/000077500000000000000000000000001446124364700142635ustar00rootroot00000000000000table_log-0.6.4/sql/table_log.sql000066400000000000000000000177371446124364700167530ustar00rootroot00000000000000CREATE EXTENSION table_log; SET client_min_messages TO warning; -- drop old trigger DROP TRIGGER test_log_chg ON test; -- ignore any error -- create demo table DROP TABLE test; -- ignore any error CREATE TABLE test ( id INT NOT NULL PRIMARY KEY, name VARCHAR(20) NOT NULL ); -- create the table without data from demo table DROP TABLE test_log; -- ignore any error SELECT * INTO test_log FROM test LIMIT 0; ALTER TABLE test_log ADD COLUMN trigger_mode VARCHAR(10); ALTER TABLE test_log ADD COLUMN trigger_tuple VARCHAR(5); ALTER TABLE test_log ADD COLUMN trigger_changed TIMESTAMPTZ; ALTER TABLE test_log ADD COLUMN trigger_id BIGINT; CREATE SEQUENCE test_log_id; SELECT SETVAL('test_log_id', 1, FALSE); ALTER TABLE test_log ALTER COLUMN trigger_id SET DEFAULT NEXTVAL('test_log_id'); -- create trigger CREATE TRIGGER test_log_chg AFTER UPDATE OR INSERT OR DELETE ON test FOR EACH ROW EXECUTE PROCEDURE table_log(); -- test trigger INSERT INTO test VALUES (1, 'name'); SELECT id, name FROM test; SELECT id, name, trigger_mode, trigger_tuple, trigger_id FROM test_log; UPDATE test SET name='other name' WHERE id=1; SELECT id, name FROM test; SELECT id, name, trigger_mode, trigger_tuple, trigger_id FROM test_log; -- create restore table SELECT table_log_restore_table('test', 'id', 'test_log', 'trigger_id', 'test_recover', NOW()); SELECT id, name FROM test_recover; DROP TABLE test; DROP TABLE test_log; DROP TABLE test_recover; -- test table_log_init with all arguments -- trigger_user and trigger_changed might differ, so ignore it SET client_min_messages TO warning; CREATE TABLE test(id integer, name text); SELECT table_log_init(5, 'test'); INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); INSERT INTO test VALUES(4, 'Jeanne D''Arc'); UPDATE test SET name = 'veronica' WHERE id = 3; UPDATE test SET name = 'Jeanne D''Arc updated' WHERE id = 4; DELETE FROM test WHERE id = 1; SELECT id, name, trigger_mode, trigger_tuple, trigger_id FROM test_log; DROP TABLE test; DROP TABLE test_log; DROP SEQUENCE test_log_seq; CREATE TABLE test(id integer, name text); SELECT table_log_init(4, 'test'); INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); INSERT INTO test VALUES(4, 'Jeanne D''Arc'); UPDATE test SET name = 'veronica' WHERE id = 3; UPDATE test SET name = 'Jeanne D''Arc updated' WHERE id = 4; DELETE FROM test WHERE id = 1; SELECT id, name, trigger_mode, trigger_tuple, trigger_id FROM test_log; DROP TABLE test; DROP TABLE test_log; DROP SEQUENCE test_log_seq; CREATE TABLE test(id integer, name text); SELECT table_log_init(3, 'test'); INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); INSERT INTO test VALUES(4, 'Jeanne D''Arc'); UPDATE test SET name = 'veronica' WHERE id = 3; UPDATE test SET name = 'Jeanne D''Arc updated' WHERE id = 4; DELETE FROM test WHERE id = 1; SELECT id, name, trigger_mode, trigger_tuple FROM test_log; DROP TABLE test; DROP TABLE test_log; -- Check table_log_restore_table() CREATE TABLE test(id integer, name text); ALTER TABLE test ADD PRIMARY KEY(id); SELECT table_log_init(5, 'test'); INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); INSERT INTO test VALUES(4, 'Jeanne D''Arc'); UPDATE test SET name = 'veronica' WHERE id = 3; UPDATE test SET name = 'Jeanne D''Arc updated' WHERE id = 4; DELETE FROM test WHERE id = 1; SELECT table_log_restore_table('test', 'id', 'test_log', 'trigger_id', 'test_recover', NOW(), '2', NULL::int, 1); SELECT id, name FROM test_recover; DROP TABLE test; DROP TABLE test_log; DROP TABLE test_recover; DROP SEQUENCE test_log_seq; -- Check partition support with auto-generated -- log table name. CREATE TABLE test(id integer, name text); ALTER TABLE test ADD PRIMARY KEY(id); SELECT table_log_init(5, 'public', 'test', 'public', NULL, 'PARTITION'); SET table_log.active_partition = 0; INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); SET table_log.active_partition = 1; UPDATE test SET name = 'veronica' WHERE id = 3; DELETE FROM test WHERE id = 1; SELECT id, name FROM test_log; SELECT id, name FROM test_log_0; SELECT id, name FROM test_log_1; -- -- NOTE: since we use partitioning here, we use the special log -- view to restore the data with id = 2 -- SELECT table_log_restore_table('test', 'id', 'test_log', 'trigger_id', 'test_recover', NOW(), '3', NULL::int, 1); SELECT id, name FROM test_recover; DROP TABLE test; DROP VIEW test_log; DROP TABLE test_log_0; DROP TABLE test_log_1; DROP TABLE test_recover; DROP SEQUENCE test_log_seq; -- -- Check with schema-qualified restore and log table -- CREATE SCHEMA log; CREATE TABLE test(id integer, name text); ALTER TABLE test ADD PRIMARY KEY(id); SELECT table_log_init(5, 'public', 'test', 'log', NULL, 'PARTITION'); SET table_log.active_partition = 0; INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); SET table_log.active_partition = 1; UPDATE test SET name = 'veronica' WHERE id = 3; SELECT * FROM test; DELETE FROM test WHERE id = 1; SELECT * FROM test; SELECT id, name FROM log.test_log; SELECT id, name FROM log.test_log_0; SELECT id, name FROM log.test_log_1; -- -- NOTE: since we use partitioning here, we use the special log -- view to restore the data with id = 2 -- SELECT table_log_restore_table('test', 'id', 'log.test_log', 'trigger_id', 'log.test_recover', NOW(), '3', NULL::int, 1); SELECT id, name FROM log.test_recover; DROP SCHEMA log CASCADE; DROP TABLE test; -- -- Test with a schema-qualified and case sensitive log and -- restore table -- CREATE SCHEMA log; CREATE TABLE test(id integer, name text); ALTER TABLE test ADD PRIMARY KEY(id); SELECT table_log_init(5, 'public', 'test', 'log', 'Test', 'PARTITION'); -- Check for log partitions SELECT 'log."Test_0"'::regclass; SELECT 'log."Test_1"'::regclass; SET table_log.active_partition = 0; INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); SET table_log.active_partition = 1; UPDATE test SET name = 'veronica' WHERE id = 3; SELECT * FROM test; DELETE FROM test WHERE id = 1; SELECT * FROM test; SELECT id, name FROM log."Test"; SELECT id, name FROM log."Test_0"; SELECT id, name FROM log."Test_1"; -- -- NOTE: since we use partitioning here, we use the special log -- view to restore the data with id = 2 -- SELECT table_log_restore_table('test', 'id', 'log."Test"', 'trigger_id', 'log."Test_recover"', NOW(), '3', NULL::int, 1); SELECT id, name FROM log."Test_recover"; DROP SCHEMA log CASCADE; DROP TABLE test; -- -- Test basic log mode -- CREATE SCHEMA log; CREATE TABLE test(id integer, name text); ALTER TABLE test ADD PRIMARY KEY(id); -- this should fail, no trigger actions specified SELECT table_log_init(5, 'public', 'test', 'log', NULL, 'PARTITION', true, '{}'); -- this should succeed, but leave out inserts -- generate the log table this time... SELECT table_log_init(5, 'public', 'test', 'log', NULL, 'PARTITION', true, '{UPDATE, DELETE}'); -- Log partitions created? SELECT 'log.test_log_0'::regclass; SELECT 'log.test_log_1'::regclass; SET table_log.active_partition = 0; INSERT INTO test VALUES(1, 'joe'); INSERT INTO test VALUES(2, 'barney'); INSERT INTO test VALUES(3, 'monica'); SET table_log.active_partition = 1; UPDATE test SET name = 'veronica' WHERE id = 3; SELECT * FROM test; DELETE FROM test WHERE id = 1; SELECT * FROM test; -- UPDATE logged only, but only old tuples SELECT id, name FROM log.test_log; SELECT id, name FROM log.test_log_0; SELECT id, name FROM log.test_log_1; -- NOTE: -- We don't test table_log_restore_table() at this point, since -- restore from data collected by table_log_basic() is not yet supported. DROP SCHEMA log CASCADE; DROP TABLE test; RESET client_min_messages; table_log-0.6.4/table_log--0.5--0.6.1.sql000066400000000000000000000110461446124364700172310ustar00rootroot00000000000000-- Remove old version of table_log_init() DROP FUNCTION table_log_init(int, text, text, text, text); CREATE FUNCTION table_log_basic() RETURNS TRIGGER AS 'MODULE_PATHNAME' LANGUAGE C; CREATE OR REPLACE FUNCTION table_log_init(int, text, text, text, text, text DEFAULT 'SINGLE', boolean DEFAULT false, text[] DEFAULT '{INSERT, UPDATE, DELETE}'::text[]) RETURNS void AS $table_log_init$ DECLARE level ALIAS FOR $1; orig_schema ALIAS FOR $2; orig_name ALIAS FOR $3; log_schema ALIAS FOR $4; log_name ALIAS FOR $5; partition_mode ALIAS FOR $6; basic_mode ALIAS FOR $7; log_actions ALIAS FOR $8; do_log_user int = 0; level_create text = ''; orig_qq text; log_qq text; log_part text[]; log_seq text; num_log_tables integer; trigger_func text := 'table_log'; trigger_actions text := ''; i integer; BEGIN -- Handle if someone doesn't want an explicit log table name log_name := COALESCE(log_name, orig_name || '_log'); -- Quoted qualified names orig_qq := quote_ident(orig_schema) || '.' || quote_ident(orig_name); log_qq := quote_ident(log_schema) || '.' || quote_ident(log_name); log_seq := quote_ident(log_schema) || '.' || quote_ident(log_name || '_seq'); log_part[0] := quote_ident(log_schema) || '.' || quote_ident(log_name || '_0'); log_part[1] := quote_ident(log_schema) || '.' || quote_ident(log_name || '_1'); -- Valid trigger actions? IF (COALESCE(array_length(log_actions, 1), 0) = 0) THEN RAISE EXCEPTION 'table_log_init: at least one trigger action must be specified'; END IF; -- Valid partition mode ? IF (partition_mode NOT IN ('SINGLE', 'PARTITION')) THEN RAISE EXCEPTION 'table_log_init: unsupported partition mode %', partition_mode; END IF; IF level <> 3 THEN -- -- Create a sequence used by trigger_id, if requested. -- EXECUTE 'CREATE SEQUENCE ' || log_seq; level_create := level_create || ', trigger_id BIGINT' || ' DEFAULT nextval($$' || log_seq || '$$::regclass)' || ' NOT NULL PRIMARY KEY'; IF level <> 4 THEN level_create := level_create || ', trigger_user VARCHAR(32) NOT NULL'; do_log_user := 1; IF level <> 5 THEN RAISE EXCEPTION 'table_log_init: First arg has to be 3, 4 or 5.'; END IF; END IF; END IF; IF (partition_mode = 'SINGLE') THEN EXECUTE 'CREATE TABLE ' || log_qq || '(LIKE ' || orig_qq || ', trigger_mode VARCHAR(10) NOT NULL' || ', trigger_tuple VARCHAR(5) NOT NULL' || ', trigger_changed TIMESTAMPTZ NOT NULL' || level_create || ')'; ELSE -- Partitioned mode requested... EXECUTE 'CREATE TABLE ' || log_part[0] || '(LIKE ' || orig_qq || ', trigger_mode VARCHAR(10) NOT NULL' || ', trigger_tuple VARCHAR(5) NOT NULL' || ', trigger_changed TIMESTAMPTZ NOT NULL' || level_create || ')'; EXECUTE 'CREATE TABLE ' || log_part[1] || '(LIKE ' || orig_qq || ', trigger_mode VARCHAR(10) NOT NULL' || ', trigger_tuple VARCHAR(5) NOT NULL' || ', trigger_changed TIMESTAMPTZ NOT NULL' || level_create || ')'; EXECUTE 'CREATE VIEW ' || log_qq || ' AS SELECT * FROM ' || log_part[0] || ' UNION ALL ' || 'SELECT * FROM ' || log_part[1] || ''; END IF; -- -- Either use basic or full trigger mode -- IF basic_mode THEN trigger_func := 'table_log_basic'; END IF; -- -- Build action string for trigger DDL -- FOR i IN 1..array_length(log_actions, 1) LOOP trigger_actions := trigger_actions || log_actions[i]; IF i < array_length(log_actions, 1) THEN trigger_actions := trigger_actions || ' OR '; END IF; END LOOP; EXECUTE 'CREATE TRIGGER "table_log_trigger" AFTER ' || trigger_actions || ' ON ' || orig_qq || ' FOR EACH ROW EXECUTE PROCEDURE ' || trigger_func || '(' || quote_literal(log_name) || ',' || do_log_user || ',' || quote_literal(log_schema) || ',' || quote_literal(partition_mode) || ')'; RETURN; END; $table_log_init$ LANGUAGE plpgsql; table_log-0.6.4/table_log--0.6--0.6.1.sql000066400000000000000000000107011446124364700172270ustar00rootroot00000000000000CREATE FUNCTION table_log_basic() RETURNS TRIGGER AS 'MODULE_PATHNAME' LANGUAGE C; CREATE OR REPLACE FUNCTION table_log_init(int, text, text, text, text, text DEFAULT 'SINGLE', boolean DEFAULT false, text[] DEFAULT '{INSERT, UPDATE, DELETE}'::text[]) RETURNS void AS $table_log_init$ DECLARE level ALIAS FOR $1; orig_schema ALIAS FOR $2; orig_name ALIAS FOR $3; log_schema ALIAS FOR $4; log_name ALIAS FOR $5; partition_mode ALIAS FOR $6; basic_mode ALIAS FOR $7; log_actions ALIAS FOR $8; do_log_user int = 0; level_create text = ''; orig_qq text; log_qq text; log_part text[]; log_seq text; num_log_tables integer; trigger_func text := 'table_log'; trigger_actions text := ''; i integer; BEGIN -- Handle if someone doesn't want an explicit log table name log_name := COALESCE(log_name, orig_name || '_log'); -- Quoted qualified names orig_qq := quote_ident(orig_schema) || '.' || quote_ident(orig_name); log_qq := quote_ident(log_schema) || '.' || quote_ident(log_name); log_seq := quote_ident(log_schema) || '.' || quote_ident(log_name || '_seq'); log_part[0] := quote_ident(log_schema) || '.' || quote_ident(log_name || '_0'); log_part[1] := quote_ident(log_schema) || '.' || quote_ident(log_name || '_1'); -- Valid trigger actions? IF (COALESCE(array_length(log_actions, 1), 0) = 0) THEN RAISE EXCEPTION 'table_log_init: at least one trigger action must be specified'; END IF; -- Valid partition mode ? IF (partition_mode NOT IN ('SINGLE', 'PARTITION')) THEN RAISE EXCEPTION 'table_log_init: unsupported partition mode %', partition_mode; END IF; IF level <> 3 THEN -- -- Create a sequence used by trigger_id, if requested. -- EXECUTE 'CREATE SEQUENCE ' || log_seq; level_create := level_create || ', trigger_id BIGINT' || ' DEFAULT nextval($$' || log_seq || '$$::regclass)' || ' NOT NULL PRIMARY KEY'; IF level <> 4 THEN level_create := level_create || ', trigger_user VARCHAR(32) NOT NULL'; do_log_user := 1; IF level <> 5 THEN RAISE EXCEPTION 'table_log_init: First arg has to be 3, 4 or 5.'; END IF; END IF; END IF; IF (partition_mode = 'SINGLE') THEN EXECUTE 'CREATE TABLE ' || log_qq || '(LIKE ' || orig_qq || ', trigger_mode VARCHAR(10) NOT NULL' || ', trigger_tuple VARCHAR(5) NOT NULL' || ', trigger_changed TIMESTAMPTZ NOT NULL' || level_create || ')'; ELSE -- Partitioned mode requested... EXECUTE 'CREATE TABLE ' || log_part[0] || '(LIKE ' || orig_qq || ', trigger_mode VARCHAR(10) NOT NULL' || ', trigger_tuple VARCHAR(5) NOT NULL' || ', trigger_changed TIMESTAMPTZ NOT NULL' || level_create || ')'; EXECUTE 'CREATE TABLE ' || log_part[1] || '(LIKE ' || orig_qq || ', trigger_mode VARCHAR(10) NOT NULL' || ', trigger_tuple VARCHAR(5) NOT NULL' || ', trigger_changed TIMESTAMPTZ NOT NULL' || level_create || ')'; EXECUTE 'CREATE VIEW ' || log_qq || ' AS SELECT * FROM ' || log_part[0] || ' UNION ALL ' || 'SELECT * FROM ' || log_part[1] || ''; END IF; -- -- Either use basic or full trigger mode -- IF basic_mode THEN trigger_func := 'table_log_basic'; END IF; -- -- Build action string for trigger DDL -- FOR i IN 1..array_length(log_actions, 1) LOOP trigger_actions := trigger_actions || log_actions[i]; IF i < array_length(log_actions, 1) THEN trigger_actions := trigger_actions || ' OR '; END IF; END LOOP; EXECUTE 'CREATE TRIGGER "table_log_trigger" AFTER ' || trigger_actions || ' ON ' || orig_qq || ' FOR EACH ROW EXECUTE PROCEDURE ' || trigger_func || '(' || quote_literal(log_name) || ',' || do_log_user || ',' || quote_literal(log_schema) || ',' || quote_literal(partition_mode) || ')'; RETURN; END; $table_log_init$ LANGUAGE plpgsql; table_log-0.6.4/table_log--0.6.1.sql000066400000000000000000000145651446124364700166650ustar00rootroot00000000000000-- -- table_log () -- log changes to another table -- -- -- see README.md for details -- -- -- written by Andreas ' ads' Scherbaum (ads@pgug.de) -- -- -- create function CREATE FUNCTION table_log_basic() RETURNS TRIGGER AS 'MODULE_PATHNAME' LANGUAGE C; CREATE FUNCTION table_log () RETURNS TRIGGER AS 'MODULE_PATHNAME' LANGUAGE C; CREATE FUNCTION "table_log_restore_table" (VARCHAR, VARCHAR, CHAR, CHAR, CHAR, TIMESTAMPTZ, CHAR, INT, INT) RETURNS VARCHAR AS 'MODULE_PATHNAME', 'table_log_restore_table' LANGUAGE C; CREATE FUNCTION "table_log_restore_table" (VARCHAR, VARCHAR, CHAR, CHAR, CHAR, TIMESTAMPTZ, CHAR, INT) RETURNS VARCHAR AS 'MODULE_PATHNAME', 'table_log_restore_table' LANGUAGE C; CREATE FUNCTION "table_log_restore_table" (VARCHAR, VARCHAR, CHAR, CHAR, CHAR, TIMESTAMPTZ, CHAR) RETURNS VARCHAR AS 'MODULE_PATHNAME', 'table_log_restore_table' LANGUAGE C; CREATE FUNCTION "table_log_restore_table" (VARCHAR, VARCHAR, CHAR, CHAR, CHAR, TIMESTAMPTZ) RETURNS VARCHAR AS 'MODULE_PATHNAME', 'table_log_restore_table' LANGUAGE C; CREATE OR REPLACE FUNCTION table_log_init(int, text, text, text, text, text DEFAULT 'SINGLE', boolean DEFAULT false, text[] DEFAULT '{INSERT, UPDATE, DELETE}'::text[]) RETURNS void AS $table_log_init$ DECLARE level ALIAS FOR $1; orig_schema ALIAS FOR $2; orig_name ALIAS FOR $3; log_schema ALIAS FOR $4; log_name ALIAS FOR $5; partition_mode ALIAS FOR $6; basic_mode ALIAS FOR $7; log_actions ALIAS FOR $8; do_log_user int = 0; level_create text = ''; orig_qq text; log_qq text; log_part text[]; log_seq text; num_log_tables integer; trigger_func text := 'table_log'; trigger_actions text := ''; i integer; BEGIN -- Handle if someone doesn't want an explicit log table name log_name := COALESCE(log_name, orig_name || '_log'); -- Quoted qualified names orig_qq := quote_ident(orig_schema) || '.' || quote_ident(orig_name); log_qq := quote_ident(log_schema) || '.' || quote_ident(log_name); log_seq := quote_ident(log_schema) || '.' || quote_ident(log_name || '_seq'); log_part[0] := quote_ident(log_schema) || '.' || quote_ident(log_name || '_0'); log_part[1] := quote_ident(log_schema) || '.' || quote_ident(log_name || '_1'); -- Valid trigger actions? IF (COALESCE(array_length(log_actions, 1), 0) = 0) THEN RAISE EXCEPTION 'table_log_init: at least one trigger action must be specified'; END IF; -- Valid partition mode ? IF (partition_mode NOT IN ('SINGLE', 'PARTITION')) THEN RAISE EXCEPTION 'table_log_init: unsupported partition mode %', partition_mode; END IF; IF level <> 3 THEN -- -- Create a sequence used by trigger_id, if requested. -- EXECUTE 'CREATE SEQUENCE ' || log_seq; level_create := level_create || ', trigger_id BIGINT' || ' DEFAULT nextval($$' || log_seq || '$$::regclass)' || ' NOT NULL PRIMARY KEY'; IF level <> 4 THEN level_create := level_create || ', trigger_user VARCHAR(32) NOT NULL'; do_log_user := 1; IF level <> 5 THEN RAISE EXCEPTION 'table_log_init: First arg has to be 3, 4 or 5.'; END IF; END IF; END IF; IF (partition_mode = 'SINGLE') THEN EXECUTE 'CREATE TABLE ' || log_qq || '(LIKE ' || orig_qq || ', trigger_mode VARCHAR(10) NOT NULL' || ', trigger_tuple VARCHAR(5) NOT NULL' || ', trigger_changed TIMESTAMPTZ NOT NULL' || level_create || ')'; ELSE -- Partitioned mode requested... EXECUTE 'CREATE TABLE ' || log_part[0] || '(LIKE ' || orig_qq || ', trigger_mode VARCHAR(10) NOT NULL' || ', trigger_tuple VARCHAR(5) NOT NULL' || ', trigger_changed TIMESTAMPTZ NOT NULL' || level_create || ')'; EXECUTE 'CREATE TABLE ' || log_part[1] || '(LIKE ' || orig_qq || ', trigger_mode VARCHAR(10) NOT NULL' || ', trigger_tuple VARCHAR(5) NOT NULL' || ', trigger_changed TIMESTAMPTZ NOT NULL' || level_create || ')'; EXECUTE 'CREATE VIEW ' || log_qq || ' AS SELECT * FROM ' || log_part[0] || ' UNION ALL ' || 'SELECT * FROM ' || log_part[1] || ''; END IF; -- -- Either use basic or full trigger mode -- IF basic_mode THEN trigger_func := 'table_log_basic'; END IF; -- -- Build action string for trigger DDL -- FOR i IN 1..array_length(log_actions, 1) LOOP trigger_actions := trigger_actions || log_actions[i]; IF i < array_length(log_actions, 1) THEN trigger_actions := trigger_actions || ' OR '; END IF; END LOOP; EXECUTE 'CREATE TRIGGER "table_log_trigger" AFTER ' || trigger_actions || ' ON ' || orig_qq || ' FOR EACH ROW EXECUTE PROCEDURE ' || trigger_func || '(' || quote_literal(log_name) || ',' || do_log_user || ',' || quote_literal(log_schema) || ',' || quote_literal(partition_mode) || ')'; RETURN; END; $table_log_init$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION table_log_init(int, text) RETURNS void AS ' DECLARE level ALIAS FOR $1; orig_name ALIAS FOR $2; BEGIN PERFORM table_log_init(level, orig_name, current_schema()); RETURN; END; ' LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION table_log_init(int, text, text) RETURNS void AS ' DECLARE level ALIAS FOR $1; orig_name ALIAS FOR $2; log_schema ALIAS FOR $3; BEGIN PERFORM table_log_init(level, current_schema(), orig_name, log_schema); RETURN; END; ' LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION table_log_init(int, text, text, text) RETURNS void AS ' DECLARE level ALIAS FOR $1; orig_schema ALIAS FOR $2; orig_name ALIAS FOR $3; log_schema ALIAS FOR $4; BEGIN PERFORM table_log_init(level, orig_schema, orig_name, log_schema, CASE WHEN orig_schema=log_schema THEN orig_name||''_log'' ELSE orig_name END); RETURN; END; ' LANGUAGE plpgsql; table_log-0.6.4/table_log--unpackaged--0.6.1.sql000066400000000000000000000147501446124364700210360ustar00rootroot00000000000000ALTER EXTENSION table_log ADD FUNCTION table_log(); ALTER EXTENSION table_log ADD FUNCTION table_log_restore_table(varchar, varchar, char, char, char, timestamp with time zone, char, integer, integer); ALTER EXTENSION table_log ADD FUNCTION table_log_restore_table(varchar, varchar, char, char, char, timestamp with time zone, char, integer); ALTER EXTENSION table_log ADD FUNCTION table_log_restore_table(varchar, varchar, char, char, char, timestamp with time zone, char); ALTER EXTENSION table_log ADD FUNCTION table_log_restore_table(varchar, varchar, char, char, char, timestamp with time zone); -- -- NOTE: -- -- When upgrading from 'unpackaged' we assume that the original -- version is an old style contrib installation with table_log -- 0.4 or below. This version doesn't have the six argument -- version of table_log_init()...so drop the old one and recreate -- the new version from scratch. -- DROP FUNCTION FUNCTION table_log_init(integer, text, text, text, text); -- Create new version of table_log_init() having the new default partition mode -- parameter. CREATE OR REPLACE FUNCTION table_log_init(int, text, text, text, text, text DEFAULT 'SINGLE') RETURNS void AS $table_log_init$ DECLARE level ALIAS FOR $1; orig_schema ALIAS FOR $2; orig_name ALIAS FOR $3; log_schema ALIAS FOR $4; log_name ALIAS FOR $5; do_log_user int = 0; level_create text = ''; orig_qq text; log_qq text; partition_mode ALIAS FOR $6; num_log_tables integer; BEGIN -- Quoted qualified names orig_qq := quote_ident(orig_schema) || '.' ||quote_ident(orig_name); log_qq := quote_ident(log_schema) || '.' ||quote_ident(log_name); -- Valid partition mode ? IF (partition_mode NOT IN ('SINGLE', 'PARTITION')) THEN RAISE EXCEPTION 'table_log_init: unsupported partition mode %', partition_mode; END IF; IF level <> 3 THEN level_create := level_create || ', trigger_id BIGSERIAL NOT NULL PRIMARY KEY'; IF level <> 4 THEN level_create := level_create || ', trigger_user VARCHAR(32) NOT NULL'; do_log_user := 1; IF level <> 5 THEN RAISE EXCEPTION 'table_log_init: First arg has to be 3, 4 or 5.'; END IF; END IF; END IF; IF (partition_mode = 'SINGLE') THEN EXECUTE 'CREATE TABLE ' || log_qq || '(LIKE ' || orig_qq || ', trigger_mode VARCHAR(10) NOT NULL' || ', trigger_tuple VARCHAR(5) NOT NULL' || ', trigger_changed TIMESTAMPTZ NOT NULL' || level_create || ')'; ELSE -- Partitioned mode requested... EXECUTE 'CREATE TABLE ' || log_qq || '_0' || '(LIKE ' || orig_qq || ', trigger_mode VARCHAR(10) NOT NULL' || ', trigger_tuple VARCHAR(5) NOT NULL' || ', trigger_changed TIMESTAMPTZ NOT NULL' || level_create || ')'; EXECUTE 'CREATE TABLE ' || log_qq || '_1' || '(LIKE ' || orig_qq || ', trigger_mode VARCHAR(10) NOT NULL' || ', trigger_tuple VARCHAR(5) NOT NULL' || ', trigger_changed TIMESTAMPTZ NOT NULL' || level_create || ')'; EXECUTE 'CREATE VIEW ' || log_qq || '_v' || ' AS SELECT * FROM ' || log_qq || '_0 UNION ALL ' || 'SELECT * FROM ' || log_qq || '_1'; END IF; EXECUTE 'CREATE TRIGGER "table_log_trigger" AFTER UPDATE OR INSERT OR DELETE ON ' || orig_qq || ' FOR EACH ROW EXECUTE PROCEDURE table_log(' || quote_literal(log_name) || ',' || do_log_user || ',' || quote_literal(log_schema) || ',' || quote_literal(partition_mode) || ')'; RETURN; END; $table_log_init$ LANGUAGE plpgsql; ALTER EXTENSION table_log ADD FUNCTION table_log_init(integer, text, text, text, text, text DEFAULT 'SINGLE'); ALTER EXTENSION table_log ADD FUNCTION table_log_init(integer, text); ALTER EXTENSION table_log ADD FUNCTION table_log_init(integer, text, text); ALTER EXTENSION table_log ADD FUNCTION table_log_init(integer, text, text, text); table_log-0.6.4/table_log.c000066400000000000000000001427611446124364700155730ustar00rootroot00000000000000/* * table_log () -- log changes to another table * * * see README.md for details * * * written by Andreas ' ads' Scherbaum (ads@pgug.de) * */ #include "table_log.h" #include /* tolower () */ #include /* strlen() */ #include "postgres.h" #include "fmgr.h" #include "executor/spi.h" /* this is what you need to work with SPI */ #include "catalog/namespace.h" #include "commands/trigger.h" /* -"- and triggers */ #include "mb/pg_wchar.h" /* support for the quoting functions */ #include "miscadmin.h" #include "lib/stringinfo.h" #include "utils/builtins.h" #include "utils/formatting.h" #include "utils/guc.h" #include #include #include #include #include "funcapi.h" #if PG_VERSION_NUM >= 90300 #include "access/htup_details.h" #endif #if PG_VERSION_NUM >= 100000 #include "utils/varlena.h" /* SplitIdentifierString */ #endif #if PG_VERSION_NUM >= 120000 #include "access/table.h" #endif #if PG_VERSION_NUM < 100000 /* from src/include/access/tupdesc.h, introduced in 2cd708452 */ #define TupleDescAttr(tupdesc, i) ((tupdesc)->attrs[(i)]) #endif /* for PostgreSQL >= 8.2.x */ #ifdef PG_MODULE_MAGIC PG_MODULE_MAGIC; #endif #ifndef PG_NARGS /* * Get number of arguments passed to function. * this macro isnt defined in 7.2.x */ #define PG_NARGS() (fcinfo->nargs) #endif /* * Current active log table partition. Default is always zero. */ TableLogPartitionId tableLogActivePartitionId = 0; /* * table_log restore descriptor. * * Carries all information to restore data from a table_log table */ typedef struct { char *schema; char *relname; } TableLogRelIdent; /* * table_log logging descriptor for log triggers. */ typedef struct { /* * Pointer to trigger data */ TriggerData *trigdata; /* * Number of columns of source table (excludes * dropped columns!) */ int number_columns; /* * Number of columns of log table (excludes * dropped columns!) */ int number_columns_log; /* * Name/schema of the log table */ TableLogRelIdent ident_log; /* * Log session user */ int use_session_user; } TableLogDescr; /* * table_log restore descriptor structure. */ typedef struct { /* Non-qualified relation name * of original table. */ char *orig_relname; /* * OID of original table, saved * for cache lookup. */ Oid orig_relid; /* * List of attnums of original table part * of the primary key or unique constraint. Only * used in case of no explicit specified pk column. * (see relationGetPrimaryKeyColumns() for details). */ AttrNumber *orig_pk_attnum; /* * Number of pk attributes in original tables. */ int orig_num_pk_attnums; /* * List of attribute names. The list index matches * the attribute number stored in the orig_pk_attnum * array. */ List *orig_pk_attr_names; /* * OID of log table. */ Oid log_relid; /* * Possible schema qualified relation name * of log table. */ bool use_schema_log; union { TableLogRelIdent ident_log; char *relname_log; }; /* * Primary key column name of the log table. */ char *pkey_log; /* * OID of restore table. */ Oid restore_relid; /* * Possible schema qualified relation name * of restore table. */ bool use_schema_restore; union { TableLogRelIdent ident_restore; char *relname_restore; }; } TableLogRestoreDescr; #define DESCR_TRIGDATA(a) \ (a).trigdata #define DESCR_TRIGDATA_GET_TUPDESC(a) \ (a).trigdata->tg_relation->rd_att #define DESCR_TRIGDATA_GET_RELATION(a) \ (a).trigdata->tg_relation #define DESCR_TRIGDATA_GETARG(a, index) \ (a).trigdata->tg_trigger->tgargs[(index)] #define DESCR_TRIGDATA_NARGS(a) \ (a).trigdata->tg_trigger->tgnargs #define DESCR_TRIGDATA_GET_TUPLE(a) \ (a).trigdata->tg_trigtuple #define DESCR_TRIGDATA_GET_NEWTUPLE(a) \ (a).trigdata->tg_newtuple #define DESCR_TRIGDATA_LOG_SCHEMA(a) \ (a).trigdata->tg_trigger->tgargs[2] #define DESCR_TRIGDATA_LOG_SESSION_USER(a) \ (a).trigdata->tg_trigger->tgargs[1] #define RESTORE_TABLE_IDENT(a, type) \ ((a.use_schema_##type ) \ ? quote_qualified_identifier(a.ident_##type.schema, \ a.ident_##type.relname)\ : quote_identifier(a.relname_##type)) #if PG_VERSION_NUM < 90300 #define TABLE_LOG_NSPOID(a) LookupExplicitNamespace((a)); #else #define TABLE_LOG_NSPOID(a) LookupExplicitNamespace((a), false); #endif void _PG_init(void); Datum table_log(PG_FUNCTION_ARGS); Datum table_log_basic(PG_FUNCTION_ARGS); Datum table_log_restore_table(PG_FUNCTION_ARGS); static char *do_quote_ident(char *iptr); static char *do_quote_literal(char *iptr); static void __table_log (TableLogDescr *descr, char *changed_mode, char *changed_tuple, HeapTuple tuple); static void table_log_prepare(TableLogDescr *descr); static void table_log_finalize(void); static void __table_log_restore_table_insert(SPITupleTable *spi_tuptable, char *table_restore, char *table_orig_pkey, char *col_query_start, int col_pkey, int number_columns, int i); static void __table_log_restore_table_update(SPITupleTable *spi_tuptable, char *table_restore, char *table_orig_pkey, char *col_query_start, int col_pkey, int number_columns, int i, char *old_key_string); static void __table_log_restore_table_delete(SPITupleTable *spi_tuptable, char *table_restore, char *table_orig_pkey, char *col_query_start, int col_pkey, int number_columns, int i); static char *__table_log_varcharout(VarChar *s); static int count_columns (TupleDesc tupleDesc); static void mapPrimaryKeyColumnNames(TableLogRestoreDescr *restore_descr); static void setTableLogRestoreDescr(TableLogRestoreDescr *restore_descr, char *table_orig, char *table_orig_pkey, char *table_log, char *table_log_pkey, char *table_restore); static void getRelationPrimaryKeyColumns(TableLogRestoreDescr *restore_descr); /* this is a V1 (new) function */ /* the trigger function */ PG_FUNCTION_INFO_V1(table_log); PG_FUNCTION_INFO_V1(table_log_basic); PG_FUNCTION_INFO_V1(table_log_forward); /* build only, if the 'Table Function API' is available */ #ifdef FUNCAPI_H_not_implemented /* restore one single column */ PG_FUNCTION_INFO_V1(table_log_show_column); #endif /* FUNCAPI_H */ /* restore a full table */ PG_FUNCTION_INFO_V1(table_log_restore_table); /* * Initialize table_log module and various internal * settings like customer variables. */ void _PG_init(void) { DefineCustomIntVariable("table_log.active_partition", "Sets the current active partition identifier.", NULL, &tableLogActivePartitionId, 0, 0, MAX_TABLE_LOG_PARTITIONS - 1, PGC_SUSET, 0, NULL, NULL, NULL); } /* * Returns a fully formatted log table relation name * of the current active log table partition. * * The table_name argument is adjusted to match either a single * table or a partitioned log table with _n appended, where n matches * the current selected active partition id * (see tableLogActivePartitionId). */ static inline char *getActiveLogTable(TriggerData *tg_data) { bool use_partitions = false; StringInfo buf = makeStringInfo(); /* * If we use several partitions for the log table, append * the partition id. */ if (tg_data->tg_trigger->tgnargs == 4) { /* * Examine trigger argument list. We expect the * partition mode to be the 4th argument to the table_log() * trigger. In case no argument was specified, we know that * we are operating on an old version, so assume * a non-partitioned installation automatically. */ if (strcmp(tg_data->tg_trigger->tgargs[3], "PARTITION") == 0) { /* Partition support enabled */ use_partitions = true; } } if (tg_data->tg_trigger->tgnargs > 0) { appendStringInfoString(buf, tg_data->tg_trigger->tgargs[0]); } else { /* * We must deal with no arguments given to the trigger. In this * case the log table name is the same like the table we are * called on, plus the _log appended... */ appendStringInfo(buf, "%s_log", SPI_getrelname(tg_data->tg_relation)); } if (use_partitions) { /* * Append the current active partition id, if partitioning * support is used. */ appendStringInfo(buf, "_%u", tableLogActivePartitionId); } /* ...and we're done */ return buf->data; } /* * count_columns (TupleDesc tupleDesc) * Will count and return the number of columns in the table described by * tupleDesc. It needs to ignore dropped columns. */ static int count_columns (TupleDesc tupleDesc) { int count = 0; int i; for (i = 0; i < tupleDesc->natts; ++i) { if (!TupleDescAttr(tupleDesc, i)->attisdropped) { ++count; } } return count; } /* * Initialize a TableLogDescr descriptor structure. */ static void initTableLogDescr(TableLogDescr *descr, TriggerData *trigdata) { Assert(descr != NULL); descr->trigdata = trigdata; descr->number_columns = -1; descr->number_columns_log = -1; descr->ident_log.schema = NULL; descr->ident_log.relname = NULL; descr->use_session_user = 0; } /* * table_log_internal() * * Internal function to initialize all required stuff * for table_log() or table_log_basic(). * * Requires a TableLogDescr structure previously * initialized via initTableLogDescr(). */ static void table_log_prepare(TableLogDescr *descr) { int ret; StringInfo query; /* must only be called for ROW trigger */ if (TRIGGER_FIRED_FOR_STATEMENT(descr->trigdata->tg_event)) { elog(ERROR, "table_log: can't process STATEMENT events"); } /* must only be called AFTER */ if (TRIGGER_FIRED_BEFORE(descr->trigdata->tg_event)) { elog(ERROR, "table_log: must be fired after event"); } /* now connect to SPI manager */ ret = SPI_connect(); if (ret != SPI_OK_CONNECT) { elog(ERROR, "table_log: SPI_connect returned %d", ret); } elog(DEBUG2, "prechecks done, now getting original table attributes"); descr->number_columns = count_columns(DESCR_TRIGDATA_GET_TUPDESC((*descr))); if (descr->number_columns < 1) { elog(ERROR, "table_log: number of columns in table is < 1, can this happen?"); } elog(DEBUG2, "number columns in orig table: %i", descr->number_columns); if (DESCR_TRIGDATA_NARGS((*descr)) > 4) { elog(ERROR, "table_log: too many arguments to trigger"); } /* name of the log schema */ if (DESCR_TRIGDATA_NARGS((*descr)) <= 2) { /* if no explicit schema specified, use source table schema */ descr->ident_log.schema = get_namespace_name(RelationGetNamespace(DESCR_TRIGDATA_GET_RELATION((*descr)))); } else { descr->ident_log.schema = DESCR_TRIGDATA_LOG_SCHEMA((*descr)); } /* name of the log table */ descr->ident_log.relname = getActiveLogTable(DESCR_TRIGDATA((*descr))); /* should we write the current user? */ if (DESCR_TRIGDATA_NARGS((*descr)) > 1) { /* * check if a second argument is given * if yes, use it, if it is true */ if (atoi(DESCR_TRIGDATA_LOG_SESSION_USER((*descr))) == 1) { descr->use_session_user = 1; elog(DEBUG2, "will write session user to 'trigger_user'"); } } elog(DEBUG2, "log table: %s.%s", quote_identifier(descr->ident_log.schema), quote_identifier(descr->ident_log.relname)); /* get the number columns in the table */ query = makeStringInfo(); appendStringInfo(query, "%s.%s", do_quote_ident(descr->ident_log.schema), do_quote_ident(descr->ident_log.relname)); descr->number_columns_log = count_columns(RelationNameGetTupleDesc(query->data)); if (descr->number_columns_log < 1) { elog(ERROR, "could not get number columns in relation %s.%s", quote_identifier(descr->ident_log.schema), quote_identifier(descr->ident_log.relname)); } elog(DEBUG2, "number columns in log table: %i", descr->number_columns_log); /* * check if the logtable has 3 (or now 4) columns more than our table * +1 if we should write the session user */ if (descr->use_session_user == 0) { /* without session user */ if ((descr->number_columns_log != descr->number_columns + 3) && (descr->number_columns_log != descr->number_columns + 4)) { elog(ERROR, "number colums in relation %s(%d) does not match columns in %s.%s(%d)", SPI_getrelname(DESCR_TRIGDATA_GET_RELATION((*descr))), descr->number_columns, quote_identifier(descr->ident_log.schema), quote_identifier(descr->ident_log.relname), descr->number_columns_log); } } else { /* with session user */ if ((descr->number_columns_log != descr->number_columns + 3 + 1) && (descr->number_columns_log != descr->number_columns + 4 + 1)) { elog(ERROR, "number colums in relation %s does not match columns in %s.%s", SPI_getrelname(DESCR_TRIGDATA_GET_RELATION((*descr))), quote_identifier(descr->ident_log.schema), quote_identifier(descr->ident_log.relname)); } } elog(DEBUG2, "log table OK"); /* For each column in key ... */ elog(DEBUG2, "copy data ..."); } static void table_log_finalize() { /* ...for now only SPI needs to be cleaned up. */ SPI_finish(); } /* * table_log_forward * * Trigger function with the same core functionality * than table_log(), but without the possibility to do * backward log replay. This means that NEW tuples for UPDATE * actions aren't logged, which makes the log table much smaller * in case someone have a heavy updated source table. */ Datum table_log_basic(PG_FUNCTION_ARGS) { TableLogDescr log_descr; /* * Some checks first... */ elog(DEBUG2, "start table_log()"); /* called by trigger manager? */ if (!CALLED_AS_TRIGGER(fcinfo)) { elog(ERROR, "table_log: not fired by trigger manager"); } /* * Assign trigger data structure to table log descriptor. */ initTableLogDescr(&log_descr, (TriggerData *) fcinfo->context); /* * Do all the preparing leg work... */ table_log_prepare(&log_descr); if (TRIGGER_FIRED_BY_INSERT(DESCR_TRIGDATA(log_descr)->tg_event)) { /* trigger called from INSERT */ elog(DEBUG2, "mode: INSERT -> new"); __table_log(&log_descr, "INSERT", "new", DESCR_TRIGDATA_GET_TUPLE(log_descr)); } else if (TRIGGER_FIRED_BY_UPDATE(DESCR_TRIGDATA(log_descr)->tg_event)) { elog(DEBUG2, "mode: UPDATE -> old"); __table_log(&log_descr, "UPDATE", "old", DESCR_TRIGDATA_GET_TUPLE(log_descr)); } else if (TRIGGER_FIRED_BY_DELETE(DESCR_TRIGDATA(log_descr)->tg_event)) { /* trigger called from DELETE */ elog(DEBUG2, "mode: DELETE -> old"); __table_log(&log_descr, "DELETE", "old", DESCR_TRIGDATA_GET_TUPLE(log_descr)); } else { elog(ERROR, "trigger fired by unknown event"); } elog(DEBUG2, "cleanup, trigger done"); table_log_finalize(); /* return trigger data */ return PointerGetDatum(DESCR_TRIGDATA_GET_TUPLE(log_descr)); } /* table_log() trigger function for logging table changes parameter: - log table name (optional) return: - trigger data (for Pg) */ Datum table_log(PG_FUNCTION_ARGS) { TableLogDescr log_descr; /* * Some checks first... */ elog(DEBUG2, "start table_log()"); /* called by trigger manager? */ if (!CALLED_AS_TRIGGER(fcinfo)) { elog(ERROR, "table_log: not fired by trigger manager"); } /* * Assign trigger data structure to table log descriptor. */ initTableLogDescr(&log_descr, (TriggerData *) fcinfo->context); /* * Do all the preparing leg work... */ table_log_prepare(&log_descr); if (TRIGGER_FIRED_BY_INSERT(DESCR_TRIGDATA(log_descr)->tg_event)) { /* trigger called from INSERT */ elog(DEBUG2, "mode: INSERT -> new"); __table_log(&log_descr, "INSERT", "new", DESCR_TRIGDATA_GET_TUPLE(log_descr)); } else if (TRIGGER_FIRED_BY_UPDATE(DESCR_TRIGDATA(log_descr)->tg_event)) { /* trigger called from UPDATE */ elog(DEBUG2, "mode: UPDATE -> old"); __table_log(&log_descr, "UPDATE", "old", DESCR_TRIGDATA_GET_TUPLE(log_descr)); elog(DEBUG2, "mode: UPDATE -> new"); __table_log(&log_descr, "UPDATE", "new", DESCR_TRIGDATA_GET_NEWTUPLE(log_descr)); } else if (TRIGGER_FIRED_BY_DELETE(DESCR_TRIGDATA(log_descr)->tg_event)) { /* trigger called from DELETE */ elog(DEBUG2, "mode: DELETE -> old"); __table_log(&log_descr, "DELETE", "old", DESCR_TRIGDATA_GET_TUPLE(log_descr)); } else { elog(ERROR, "trigger fired by unknown event"); } elog(DEBUG2, "cleanup, trigger done"); table_log_finalize(); /* return trigger data */ return PointerGetDatum(DESCR_TRIGDATA_GET_TUPLE(log_descr)); } /* __table_log() helper function for table_log() parameter: - trigger data - change mode (INSERT, UPDATE, DELETE) - tuple to log (old, new) - pointer to tuple - number columns in table - logging table - flag for writing session user return: none */ static void __table_log (TableLogDescr *descr, char *changed_mode, char *changed_tuple, HeapTuple tuple) { StringInfo query; char *before_char; int i; int col_nr; int found_col; int ret; elog(DEBUG2, "build query"); /* allocate memory */ query = makeStringInfo(); /* build query */ appendStringInfo(query, "INSERT INTO %s.%s (", do_quote_ident(descr->ident_log.schema), do_quote_ident(descr->ident_log.relname)); /* add colum names */ col_nr = 0; for (i = 1; i <= descr->number_columns; i++) { col_nr++; found_col = 0; do { if (TupleDescAttr(DESCR_TRIGDATA_GET_TUPDESC(*descr), col_nr - 1)->attisdropped) { /* this column is dropped, skip it */ col_nr++; continue; } else { found_col++; } } while (found_col == 0); appendStringInfo(query, "%s, ", do_quote_ident(SPI_fname(DESCR_TRIGDATA_GET_TUPDESC((*descr)), col_nr))); } /* add session user */ if (descr->use_session_user == 1) appendStringInfo(query, "trigger_user, "); /* add the 3 extra colum names */ appendStringInfo(query, "trigger_mode, trigger_tuple, trigger_changed) VALUES ("); /* add values */ col_nr = 0; for (i = 1; i <= descr->number_columns; i++) { col_nr++; found_col = 0; do { if (TupleDescAttr(DESCR_TRIGDATA_GET_TUPDESC(*descr), col_nr - 1)->attisdropped) { /* this column is dropped, skip it */ col_nr++; continue; } else { found_col++; } } while (found_col == 0); before_char = SPI_getvalue(tuple, DESCR_TRIGDATA_GET_TUPDESC((*descr)), col_nr); if (before_char == NULL) { appendStringInfo(query, "NULL, "); } else { appendStringInfo(query, "%s, ", do_quote_literal(before_char)); } } /* add session user */ if (descr->use_session_user == 1) appendStringInfo(query, "SESSION_USER, "); /* add the 3 extra values */ appendStringInfo(query, "%s, %s, NOW());", do_quote_literal(changed_mode), do_quote_literal(changed_tuple)); elog(DEBUG3, "query: %s", query->data); elog(DEBUG2, "execute query"); /* execute insert */ ret = SPI_exec(query->data, 0); if (ret != SPI_OK_INSERT) { elog(ERROR, "could not insert log information into relation %s.%s (error: %d)", quote_identifier(descr->ident_log.schema), quote_identifier(descr->ident_log.relname), ret); } /* clean up */ pfree(query->data); pfree(query); elog(DEBUG2, "done"); } #ifdef FUNCAPI_H_not_implemented /* table_log_show_column() show a single column on a date in the past parameter: not yet defined return: not yet defined */ Datum table_log_show_column(PG_FUNCTION_ARGS) { TriggerData *trigdata = (TriggerData *) fcinfo->context; int ret; /* * Some checks first... */ elog(DEBUG2, "start table_log_show_column()"); /* Connect to SPI manager */ ret = SPI_connect(); if (ret != SPI_OK_CONNECT) { elog(ERROR, "table_log_show_column: SPI_connect returned %d", ret); } elog(DEBUG2, "this function isnt available yet"); /* close SPI connection */ SPI_finish(); return PG_RETURN_NULL; } #endif /* FUNCAPI_H */ /* * Retrieves the columns of the primary key the original * table has and stores their attribute numbers in the * specified TableLogRestoreDescr descriptor. The caller is responsible * to pass a valid descriptor initialized by initTableLogRestoreDescr(). */ static void getRelationPrimaryKeyColumns(TableLogRestoreDescr *restore_descr) { Relation origRel; List *indexOidList; ListCell *indexOidScan; Assert((restore_descr != NULL) && (restore_descr->orig_relname != NULL)); restore_descr->orig_pk_attnum = NULL; /* * Get all indexes for the relation, take care to * request a share lock before. */ #if PG_VERSION_NUM >= 120000 origRel = table_open(restore_descr->orig_relid, AccessShareLock); #else origRel = heap_open(restore_descr->orig_relid, AccessShareLock); #endif indexOidList = RelationGetIndexList(origRel); foreach(indexOidScan, indexOidList) { Oid indexOid = lfirst_oid(indexOidScan); Form_pg_index indexStruct; HeapTuple indexTuple; int i; /* * Lookup the key via syscache, extract the key columns * from this index in case we have found a primary key. */ indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexOid)); if (!HeapTupleIsValid(indexTuple)) elog(ERROR, "cache lookup failed for index %u", indexOid); indexStruct = (Form_pg_index) GETSTRUCT(indexTuple); /* * Next one if this is not a primary key or * unique constraint. */ if (!indexStruct->indisprimary) { ReleaseSysCache(indexTuple); continue; } /* * Okay, looks like this is a PK let's * get the attnums from it and store them * in the TableLogRestoreDescr descriptor. */ restore_descr->orig_num_pk_attnums = indexStruct->indnatts; restore_descr->orig_pk_attnum = (AttrNumber *) palloc(indexStruct->indnatts * sizeof(AttrNumber)); for (i = 0; i < indexStruct->indnatts; i++) { restore_descr->orig_pk_attnum[i] = indexStruct->indkey.values[i]; } ReleaseSysCache(indexTuple); } /* * Okay, we're done. Cleanup and exit. */ #if PG_VERSION_NUM >= 120000 table_close(origRel, AccessShareLock); #else heap_close(origRel, AccessShareLock); #endif } static void setTableLogRestoreDescr(TableLogRestoreDescr *restore_descr, char *table_orig, char *table_orig_pkey, char *table_log, char *table_log_pkey, char *table_restore) { List *logIdentList; List *restoreIdentList; int i; Assert(restore_descr != NULL); /* * Setup some stuff... */ restore_descr->orig_num_pk_attnums = 0; restore_descr->orig_pk_attr_names = NIL; restore_descr->orig_pk_attnum = NULL; restore_descr->orig_relname = pstrdup(table_orig); /* * Keep gcc quiet. * In table_log_restore_table() we call RESTORE_TABLE_IDENT, * which lets gcc guess that we might get into trouble if * the restore descriptor was not fully initialized.* * * However, we already make sure that RESTORE_TABLE_IDENT * won't be called with ident_[restore|log].relname unless * use_schema_log or use_schema_restore is set to false. */ restore_descr->ident_log.relname = NULL; restore_descr->ident_restore.relname = NULL; /* * Take care for possible schema qualified relation names * in table_log and table_restore. table_orig is assumed to * be search_path aware! */ if (!SplitIdentifierString(table_restore, '.', &restoreIdentList)) { elog(ERROR, "invalid syntax for restore table name: \"%s\"", table_restore); } if (!SplitIdentifierString(table_log, '.', &logIdentList)) { elog(ERROR, "invalid syntax for log table name: \"%s\"", table_restore); } /* * Since the original table name is assumed * not to be qualified, simply look it up by RelationGetRelid() */ restore_descr->orig_relid = RelnameGetRelid(restore_descr->orig_relname); if (restore_descr->orig_relid == InvalidOid) { elog(ERROR, "lookup for relation \"%s\" failed", restore_descr->orig_relname); } /* * Assign relation identifier to restore descriptor. */ if (list_length(logIdentList) > 1) { restore_descr->ident_log.schema = (char *)lfirst(list_head(logIdentList)); restore_descr->ident_log.relname = (char *)lfirst(list_tail(logIdentList)); restore_descr->use_schema_log = true; } else { restore_descr->relname_log = table_log; restore_descr->use_schema_log = false; } if (list_length(restoreIdentList) > 1) { restore_descr->ident_restore.schema = (char *)lfirst(list_head(restoreIdentList)); restore_descr->ident_restore.relname = (char *)lfirst(list_tail(restoreIdentList)); restore_descr->use_schema_restore = true; } else { restore_descr->relname_restore = table_restore; restore_descr->use_schema_restore = false; } /* * Lookup OID of log table. TABLE_LOG_NSPOID() * takes care wether we have at least USAGE on the specified * namespace. We don't need to do that in case we have a * non-qualified relation. */ if (restore_descr->use_schema_log) { Oid nspOid; nspOid = TABLE_LOG_NSPOID((restore_descr->ident_log.schema)); restore_descr->log_relid = get_relname_relid(restore_descr->ident_log.relname, nspOid); } else { restore_descr->log_relid = RelnameGetRelid(restore_descr->relname_log); } /* * ... the same for the restore table */ if (restore_descr->use_schema_restore) { Oid nspOid; nspOid = TABLE_LOG_NSPOID((restore_descr->ident_restore.schema)); restore_descr->restore_relid = get_relname_relid(restore_descr->ident_restore.relname, nspOid); } else { restore_descr->restore_relid = RelnameGetRelid(restore_descr->relname_restore); } /* * Primary key of original table, but only in case the * caller didn't specify an explicit column. * * NOTE: This code is also responsible to support table_log_restore_table() * when having a composite primary key on a table. The old * API only allows for a single column to be specified, so to get * the new functionality the caller simply passes NULL to * the table_orig_pkey value and let this code do all the * necessary legwork. */ if (table_orig_pkey == NULL) { getRelationPrimaryKeyColumns(restore_descr); } else { /* * This is a single pkey column. */ restore_descr->orig_num_pk_attnums = 1; restore_descr->orig_pk_attnum = (AttrNumber *) palloc(sizeof(AttrNumber)); restore_descr->orig_pk_attnum[0] = get_attnum(restore_descr->orig_relid, table_orig_pkey); } /* * If there is no PK column, error out... */ if (restore_descr->orig_num_pk_attnums <= 0) elog(ERROR, "no primary key on table \"%s\" found", restore_descr->orig_relname); /* * Save the pk column name of the log table. */ restore_descr->pkey_log = pstrdup(table_log_pkey); /* * Map the attribute number for the pk to its * column names. */ mapPrimaryKeyColumnNames(restore_descr); /* * The restore table only allows for a single primary key column. * Check that this column isn't part of the original table's * pkey. */ for (i = 0; i < restore_descr->orig_num_pk_attnums; i++) { if (strncmp(list_nth(restore_descr->orig_pk_attr_names, i), restore_descr->pkey_log, NAMEDATALEN) == 0) { elog(ERROR, "primary key of log table is part of original table"); } } } /* * Takes a valid fully initialized TableLogRestoreDescr * and maps all attribute numbers from the original * table primary key to their column names. * * The caller must have called setTableLogRestoreDescr() * before. */ static void mapPrimaryKeyColumnNames(TableLogRestoreDescr *restore_descr) { int i; /* * Make sure we deal with an empty list. */ restore_descr->orig_pk_attr_names = NIL; for (i = 0; i < restore_descr->orig_num_pk_attnums; i++) { /* * Lookup the pk attribute name. */ char *pk_attr_name = pstrdup( #if PG_VERSION_NUM >= 110000 get_attname(restore_descr->orig_relid, restore_descr->orig_pk_attnum[i], false) #else get_relid_attribute_name(restore_descr->orig_relid, restore_descr->orig_pk_attnum[i]) #endif ); restore_descr->orig_pk_attr_names = lappend(restore_descr->orig_pk_attr_names, pk_attr_name); } } static inline char * StringListToString(List *list, int length, StringInfo buf) { ListCell *scan; int i; Assert(list != NIL); resetStringInfo(buf); foreach(scan, list) { char *str = (char *)lfirst(scan); appendStringInfoString(buf, str); if (i < (length - 1)) appendStringInfoString(buf, ", "); } return buf->data; } static inline char * AttrNumberArrayToString(int *attrnums, int length, StringInfo buf) { int i; Assert(attrnums != NULL); resetStringInfo(buf); for(i = 0; i < length; i++) { appendStringInfo(buf, "%d", attrnums[i]); if (i < (length - 1)) appendStringInfoString(buf, ", "); } return buf->data; } /* table_log_restore_table() restore a complete table based on the logging table parameter: - original table name - name of primary key in original table - logging table - name of primary key in logging table - restore table name - timestamp for restoring data - primary key to restore (only this key will be restored) (optional) - restore mode 0: restore from blank table (default) needs a complete logging table 1: restore from actual table backwards - dont create table temporarly 0: create restore table temporarly (default) 1: create restore table not temporarly return: not yet defined */ Datum table_log_restore_table(PG_FUNCTION_ARGS) { TableLogRestoreDescr restore_descr; /* the primary key in the original table */ char *table_orig_pkey; /* number columns in log table */ int table_log_columns = 0; /* the timestamp in past */ Datum timestamp = PG_GETARG_DATUM(5); /* the single pkey, can be null (then all keys will be restored) */ char *search_pkey = ""; /* the restore method - 0: restore from blank table (default) needs a complete log table! - 1: restore from actual table backwards */ int method = 0; /* dont create restore table temporarly - 0: create restore table temporarly (default) - 1: dont create restore table temporarly */ int not_temporarly = 0; int ret, results, i, number_columns; /* * for getting table infos */ StringInfo query; int need_search_pkey = 0; /* does we have a single key to restore? */ char *tmp, *timestamp_string, *old_pkey_string = ""; char *trigger_mode; char *trigger_tuple; char *trigger_changed; SPITupleTable *spi_tuptable = NULL; /* for saving query results */ /* memory for dynamic query */ StringInfo d_query; /* memory for column names */ StringInfo col_query; int col_pkey = 0; /* * Some checks first... */ elog(DEBUG2, "start table_log_restore_table()"); /* does we have all arguments? */ if (PG_ARGISNULL(0)) { elog(ERROR, "table_log_restore_table: missing original table name"); } if (PG_ARGISNULL(1)) { table_orig_pkey = NULL; } if (PG_ARGISNULL(2)) { elog(ERROR, "table_log_restore_table: missing log table name"); } if (PG_ARGISNULL(3)) { elog(ERROR, "table_log_restore_table: missing primary key name for log table"); } if (PG_ARGISNULL(4)) { elog(ERROR, "table_log_restore_table: missing copy table name"); } if (PG_ARGISNULL(5)) { elog(ERROR, "table_log_restore_table: missing timestamp"); } /* first check number arguments to avoid an segfault */ if (PG_NARGS() >= 7) { /* if argument is given, check if not null */ if (!PG_ARGISNULL(6)) { /* yes, fetch it */ search_pkey = __table_log_varcharout((VarChar *)PG_GETARG_VARCHAR_P(6)); /* and check, if we have an argument */ if (strlen(search_pkey) > 0) { need_search_pkey = 1; elog(DEBUG2, "table_log_restore_table: will restore a single key"); } } } /* nargs >= 7 */ /* same procedere here */ if (PG_NARGS() >= 8) { if (!PG_ARGISNULL(7)) { method = PG_GETARG_INT32(7); if (method > 0) { method = 1; } else { method = 0; } } } /* nargs >= 8 */ if (method == 1) elog(DEBUG2, "table_log_restore_table: will restore from actual state backwards"); else elog(DEBUG2, "table_log_restore_table: will restore from begin forward"); if (PG_NARGS() >= 9) { if (!PG_ARGISNULL(8)) { not_temporarly = PG_GETARG_INT32(8); if (not_temporarly > 0) { not_temporarly = 1; elog(DEBUG2, "table_log_restore_table: dont create restore table temporarly"); } else { not_temporarly = 0; } } } /* nargs >= 9 */ /* get parameter and set them to the restore descriptor */ setTableLogRestoreDescr(&restore_descr, __table_log_varcharout((VarChar *)PG_GETARG_VARCHAR_P(0)), ((table_orig_pkey != NULL) ? __table_log_varcharout((VarChar *)PG_GETARG_VARCHAR_P(1)) : NULL), __table_log_varcharout((VarChar *)PG_GETARG_VARCHAR_P(2)), __table_log_varcharout((VarChar *)PG_GETARG_VARCHAR_P(3)), __table_log_varcharout((VarChar *)PG_GETARG_VARCHAR_P(4))); /* * Composite PK not supported atm... * * CAUTION: * * The infrastructure to support composite primary keys is there, * but the following old cold still assumes there's only one column * in the PK to consider. */ if (restore_descr.orig_num_pk_attnums > 1) elog(ERROR, "composite primary key not supported"); /* Connect to SPI manager */ ret = SPI_connect(); if (ret != SPI_OK_CONNECT) { elog(ERROR, "table_log_restore_table: SPI_connect returned %d", ret); } /* check original table */ query = makeStringInfo(); appendStringInfo(query, "SELECT a.attname FROM pg_class c, pg_attribute a WHERE c.oid = %s::regclass AND a.attnum > 0 AND a.attrelid = c.oid ORDER BY a.attnum", do_quote_literal(do_quote_ident(restore_descr.orig_relname))); elog(DEBUG3, "query: %s", query->data); ret = SPI_exec(query->data, 0); if (ret != SPI_OK_SELECT) { elog(ERROR, "could not check relation: \"%s\"", restore_descr.orig_relname); } if (SPI_processed <= 0) { elog(ERROR, "could not check relation: \"%s\"", restore_descr.orig_relname); } /* check log table */ if (restore_descr.log_relid == InvalidOid) { elog(ERROR, "log table \"%s\" does not exist", RESTORE_TABLE_IDENT(restore_descr, log)); } resetStringInfo(query); appendStringInfo(query, "SELECT a.attname \ FROM pg_class c, pg_attribute a \ WHERE c.oid = %u \ AND c.relkind IN ('v', 'r') \ AND a.attnum > 0 \ AND a.attrelid = c.oid \ ORDER BY a.attnum", restore_descr.log_relid); elog(DEBUG3, "query: %s", query->data); ret = SPI_exec(query->data, 0); if (ret != SPI_OK_SELECT) { elog(ERROR, "could not check relation [1]: %s", RESTORE_TABLE_IDENT(restore_descr, log)); } if (SPI_processed <= 0) { elog(ERROR, "could not check relation [2]: %s", RESTORE_TABLE_IDENT(restore_descr, log)); } table_log_columns = SPI_processed; /* check pkey in log table */ resetStringInfo(query); appendStringInfo(query, "SELECT a.attname \ FROM pg_class c, pg_attribute a \ WHERE c.oid=%u AND c.relkind IN ('v', 'r') \ AND a.attname=%s \ AND a.attnum > 0 \ AND a.attrelid = c.oid", restore_descr.log_relid, do_quote_literal(restore_descr.pkey_log)); elog(DEBUG3, "query: %s", query->data); ret = SPI_exec(query->data, 0); if (ret != SPI_OK_SELECT) { elog(ERROR, "could not check relation [3]: %s", RESTORE_TABLE_IDENT(restore_descr, log)); } if (SPI_processed == 0) { elog(ERROR, "could not check relation [4]: %s", RESTORE_TABLE_IDENT(restore_descr, log)); } elog(DEBUG3, "log table: OK (%i columns)", table_log_columns); resetStringInfo(query); if (restore_descr.use_schema_restore) { appendStringInfo(query, "SELECT pg_attribute.attname AS a \ FROM pg_class, pg_attribute, pg_namespace \ WHERE pg_class.relname=%s \ AND pg_attribute.attnum > 0 \ AND pg_attribute.attrelid=pg_class.oid \ AND pg_namespace.nspname = %s \ AND pg_namespace.oid = pg_class.relnamespace", do_quote_literal(restore_descr.ident_restore.schema), do_quote_literal(restore_descr.ident_restore.relname)); } else { appendStringInfo(query, "SELECT pg_attribute.attname AS a \ FROM pg_class, pg_attribute \ WHERE pg_class.relname=%s \ AND pg_attribute.attnum > 0 \ AND pg_attribute.attrelid=pg_class.oid", do_quote_literal(restore_descr.relname_restore)); } elog(DEBUG3, "query: %s", query->data); ret = SPI_exec(query->data, 0); if (ret != SPI_OK_SELECT) { elog(ERROR, "could not check relation: %s", RESTORE_TABLE_IDENT(restore_descr, restore)); } if (SPI_processed > 0) { elog(ERROR, "restore table already exists: %s", RESTORE_TABLE_IDENT(restore_descr, restore)); } elog(DEBUG2, "restore table: OK (doesn't exists)"); /* now get all columns from original table */ resetStringInfo(query); appendStringInfo(query, "SELECT a.attname, format_type(a.atttypid, a.atttypmod), a.attnum \ FROM pg_class c, pg_attribute a \ WHERE c.oid = %s::regclass AND a.attnum > 0 AND a.attrelid = c.oid ORDER BY a.attnum", do_quote_literal(do_quote_ident(restore_descr.orig_relname))); elog(DEBUG3, "query: %s", query->data); ret = SPI_exec(query->data, 0); if (ret != SPI_OK_SELECT) { elog(ERROR, "could not get columns from relation: \"%s\"", restore_descr.orig_relname); } if (SPI_processed == 0) { elog(ERROR, "could not check relation: \"%s\"", restore_descr.orig_relname); } results = SPI_processed; /* store number columns for later */ number_columns = SPI_processed; elog(DEBUG2, "number columns: %i", results); for (i = 0; i < results; i++) { /* the column name */ tmp = SPI_getvalue(SPI_tuptable->vals[i], SPI_tuptable->tupdesc, 1); /* now check, if this is the pkey */ if (strcmp((const char *)tmp, (const char *)list_nth(restore_descr.orig_pk_attr_names, 0)) == 0) { /* remember the (real) number */ col_pkey = i + 1; } } /* check if we have found the pkey */ if (col_pkey == 0) { elog(ERROR, "cannot find pkey (%s) in table \"%s\"", (char *)list_nth(restore_descr.orig_pk_attr_names, 0), restore_descr.orig_relname); } /* allocate memory for string */ col_query = makeStringInfo(); for (i = 0; i < results; i++) { if (i > 0) appendStringInfo(col_query, ", "); appendStringInfo(col_query, "%s", do_quote_ident(SPI_getvalue(SPI_tuptable->vals[i], SPI_tuptable->tupdesc, 1))); } /* create restore table */ elog(DEBUG2, "string for columns: %s", col_query->data); elog(DEBUG2, "create restore table: %s", RESTORE_TABLE_IDENT(restore_descr, restore)); resetStringInfo(query); appendStringInfo(query, "SELECT * INTO "); /* per default create a temporary table */ if (not_temporarly == 0) { appendStringInfo(query, "TEMPORARY "); } /* from which table? */ appendStringInfo(query, "TABLE %s FROM %s ", RESTORE_TABLE_IDENT(restore_descr, restore), quote_identifier(restore_descr.orig_relname)); if (need_search_pkey == 1) { /* only extract a specific key */ appendStringInfo(query, "WHERE %s = %s ", do_quote_ident(list_nth(restore_descr.orig_pk_attr_names, 0)), do_quote_literal(search_pkey)); } if (method == 0) { /* restore from begin (blank table) */ appendStringInfo(query, "LIMIT 0"); } elog(DEBUG3, "query: %s", query->data); ret = SPI_exec(query->data, 0); if (ret != SPI_OK_SELINTO) { elog(ERROR, "could not check relation: %s", RESTORE_TABLE_IDENT(restore_descr, restore)); } /* get timestamp as string */ timestamp_string = DatumGetCString(DirectFunctionCall1(timestamptz_out, timestamp)); if (method == 0) elog(DEBUG2, "need logs from start to timestamp: %s", timestamp_string); else elog(DEBUG2, "need logs from end to timestamp: %s", timestamp_string); /* now build query for getting logs */ elog(DEBUG2, "build query for getting logs"); /* allocate memory for string and build query */ d_query = makeStringInfo(); elog(DEBUG2, "using log table %s", RESTORE_TABLE_IDENT(restore_descr, log)); appendStringInfo(d_query, "SELECT %s, trigger_mode, trigger_tuple, trigger_changed FROM %s WHERE ", col_query->data, RESTORE_TABLE_IDENT(restore_descr, log)); if (method == 0) { /* from start to timestamp */ appendStringInfo(d_query, "trigger_changed <= %s", do_quote_literal(timestamp_string)); } else { /* from now() backwards to timestamp */ appendStringInfo(d_query, "trigger_changed >= %s ", do_quote_literal(timestamp_string)); } if (need_search_pkey == 1) { appendStringInfo(d_query, "AND %s = %s ", do_quote_ident(list_nth(restore_descr.orig_pk_attr_names, 0)), do_quote_literal(search_pkey)); } if (method == 0) { appendStringInfo(d_query, "ORDER BY %s ASC", do_quote_ident(restore_descr.pkey_log)); } else { appendStringInfo(d_query, "ORDER BY %s DESC", do_quote_ident(restore_descr.pkey_log)); } elog(DEBUG3, "query: %s", d_query->data); ret = SPI_exec(d_query->data, 0); if (ret != SPI_OK_SELECT) { elog(ERROR, "could not get log data from table: %s", RESTORE_TABLE_IDENT(restore_descr, log)); } results = SPI_processed; /* save results */ spi_tuptable = SPI_tuptable; /* go through all results */ for (i = 0; i < results; i++) { /* get tuple data */ trigger_mode = SPI_getvalue(spi_tuptable->vals[i], spi_tuptable->tupdesc, number_columns + 1); trigger_tuple = SPI_getvalue(spi_tuptable->vals[i], spi_tuptable->tupdesc, number_columns + 2); trigger_changed = SPI_getvalue(spi_tuptable->vals[i], spi_tuptable->tupdesc, number_columns + 3); /* check for update tuples we doesnt need */ if (strcmp((const char *)trigger_mode, (const char *)"UPDATE") == 0) { if (method == 0 && strcmp((const char *)trigger_tuple, (const char *)"old") == 0) { /* we need the old value of the pkey for the update */ old_pkey_string = SPI_getvalue(spi_tuptable->vals[i], spi_tuptable->tupdesc, col_pkey); elog(DEBUG2, "tuple old pkey: %s", old_pkey_string); /* then skip this tuple */ continue; } if (method == 1 && strcmp((const char *)trigger_tuple, (const char *)"new") == 0) { /* we need the old value of the pkey for the update */ old_pkey_string = SPI_getvalue(spi_tuptable->vals[i], spi_tuptable->tupdesc, col_pkey); elog(DEBUG2, "tuple: old pkey: %s", old_pkey_string); /* then skip this tuple */ continue; } } if (method == 0) { /* roll forward */ elog(DEBUG2, "tuple: %s %s %s", trigger_mode, trigger_tuple, trigger_changed); if (strcmp((const char *)trigger_mode, (const char *)"INSERT") == 0) { __table_log_restore_table_insert(spi_tuptable, (char *)RESTORE_TABLE_IDENT(restore_descr, restore), list_nth(restore_descr.orig_pk_attr_names, 0), col_query->data, col_pkey, number_columns, i); } else if (strcmp((const char *)trigger_mode, (const char *)"UPDATE") == 0) { __table_log_restore_table_update(spi_tuptable, (char *)RESTORE_TABLE_IDENT(restore_descr, restore), list_nth(restore_descr.orig_pk_attr_names, 0), col_query->data, col_pkey, number_columns, i, old_pkey_string); } else if (strcmp((const char *)trigger_mode, (const char *)"DELETE") == 0) { __table_log_restore_table_delete(spi_tuptable, (char *)RESTORE_TABLE_IDENT(restore_descr, restore), list_nth(restore_descr.orig_pk_attr_names, 0), col_query->data, col_pkey, number_columns, i); } else { elog(ERROR, "unknown trigger_mode: %s", trigger_mode); } } else { /* roll back */ char rb_mode[10]; /* reverse the method */ if (strcmp((const char *)trigger_mode, (const char *)"INSERT") == 0) { sprintf(rb_mode, "DELETE"); } else if (strcmp((const char *)trigger_mode, (const char *)"UPDATE") == 0) { sprintf(rb_mode, "UPDATE"); } else if (strcmp((const char *)trigger_mode, (const char *)"DELETE") == 0) { sprintf(rb_mode, "INSERT"); } else { elog(ERROR, "unknown trigger_mode: %s", trigger_mode); } elog(DEBUG2, "tuple: %s %s %s", rb_mode, trigger_tuple, trigger_changed); if (strcmp((const char *)trigger_mode, (const char *)"INSERT") == 0) { __table_log_restore_table_delete(spi_tuptable, (char *)RESTORE_TABLE_IDENT(restore_descr, restore), list_nth(restore_descr.orig_pk_attr_names, 0), col_query->data, col_pkey, number_columns, i); } else if (strcmp((const char *)trigger_mode, (const char *)"UPDATE") == 0) { __table_log_restore_table_update(spi_tuptable, (char *)RESTORE_TABLE_IDENT(restore_descr, restore), list_nth(restore_descr.orig_pk_attr_names, 0), col_query->data, col_pkey, number_columns, i, old_pkey_string); } else if (strcmp((const char *)trigger_mode, (const char *)"DELETE") == 0) { __table_log_restore_table_insert(spi_tuptable, (char *)RESTORE_TABLE_IDENT(restore_descr, restore), list_nth(restore_descr.orig_pk_attr_names, 0), col_query->data, col_pkey, number_columns, i); } } } /* close SPI connection */ SPI_finish(); elog(DEBUG2, "table_log_restore_table() done, results in: %s", RESTORE_TABLE_IDENT(restore_descr, restore)); /* and return the name of the restore table */ PG_RETURN_VARCHAR_P(cstring_to_text(RESTORE_TABLE_IDENT(restore_descr, restore))); } static void __table_log_restore_table_insert(SPITupleTable *spi_tuptable, char *table_restore, char *table_orig_pkey, char *col_query_start, int col_pkey, int number_columns, int i) { int j; int ret; char *tmp; /* memory for dynamic query */ StringInfo d_query; d_query = makeStringInfo(); /* build query */ appendStringInfo(d_query, "INSERT INTO %s (%s) VALUES (", table_restore, col_query_start); for (j = 1; j <= number_columns; j++) { if (j > 1) { appendStringInfoString(d_query, ", "); } tmp = SPI_getvalue(spi_tuptable->vals[i], spi_tuptable->tupdesc, j); if (tmp == NULL) { appendStringInfoString(d_query, "NULL"); } else { appendStringInfoString(d_query, do_quote_literal(tmp)); } } appendStringInfoString(d_query, ")"); elog(DEBUG3, "query: %s", d_query->data); ret = SPI_exec(d_query->data, 0); if (ret != SPI_OK_INSERT) { elog(ERROR, "could not insert data into: %s", table_restore); } /* done */ } static void __table_log_restore_table_update(SPITupleTable *spi_tuptable, char *table_restore, char *table_orig_pkey, char *col_query_start, int col_pkey, int number_columns, int i, char *old_pkey_string) { int j; int ret; char *tmp; char *tmp2; /* memory for dynamic query */ StringInfo d_query; d_query = makeStringInfo(); /* build query */ appendStringInfo(d_query, "UPDATE %s SET ", table_restore); for (j = 1; j <= number_columns; j++) { if (j > 1) { appendStringInfoString(d_query, ", "); } tmp = SPI_getvalue(spi_tuptable->vals[i], spi_tuptable->tupdesc, j); tmp2 = SPI_fname(spi_tuptable->tupdesc, j); if (tmp == NULL) { appendStringInfo(d_query, "%s=NULL", do_quote_ident(tmp2)); } else { appendStringInfo(d_query, "%s=%s", do_quote_ident(tmp2), do_quote_literal(tmp)); } } appendStringInfo(d_query, " WHERE %s=%s", do_quote_ident(table_orig_pkey), do_quote_literal(old_pkey_string)); elog(DEBUG3, "query: %s", d_query->data); ret = SPI_exec(d_query->data, 0); if (ret != SPI_OK_UPDATE) { elog(ERROR, "could not update data in: %s", table_restore); } /* done */ } static void __table_log_restore_table_delete(SPITupleTable *spi_tuptable, char *table_restore, char *table_orig_pkey, char *col_query_start, int col_pkey, int number_columns, int i) { int ret; char *tmp; /* memory for dynamic query */ StringInfo d_query; /* get the size of value */ tmp = SPI_getvalue(spi_tuptable->vals[i], spi_tuptable->tupdesc, col_pkey); if (tmp == NULL) { elog(ERROR, "pkey cannot be NULL"); } /* initalize StringInfo structure */ d_query = makeStringInfo(); /* build query */ appendStringInfo(d_query, "DELETE FROM %s WHERE %s=%s", table_restore, do_quote_ident(table_orig_pkey), do_quote_literal(tmp)); elog(DEBUG3, "query: %s", d_query->data); ret = SPI_exec(d_query->data, 0); if (ret != SPI_OK_DELETE) { elog(ERROR, "could not delete data from: %s", table_restore); } /* done */ } static char * do_quote_ident(char *iptr) { /* Cast away const ... */ return (char *)quote_identifier(iptr); } static char * do_quote_literal(char *lptr) { return quote_literal_cstr(lptr); } static char * __table_log_varcharout(VarChar *s) { char *result; int32 len; /* copy and add null term */ len = VARSIZE(s) - VARHDRSZ; result = palloc(len + 1); memcpy(result, VARDATA(s), len); result[len] = '\0'; #ifdef CYR_RECODE convertstr(result, len, 1); #endif return result; } table_log-0.6.4/table_log.control000066400000000000000000000002001446124364700170060ustar00rootroot00000000000000comment = 'Module to log changes on tables' default_version = '0.6.1' module_pathname = '$libdir/table_log' relocatable = false table_log-0.6.4/table_log.h000066400000000000000000000006331446124364700155670ustar00rootroot00000000000000/* * table_log () -- log changes to another table * * * see README.md for details * * * written by Andreas ' ads' Scherbaum (ads@pgug.de) * adapted for PostgreSQL 9.1+ by Bernd Helmle (bernd.helmle@credativ.de) * */ #define MAX_TABLE_LOG_PARTITIONS 2 /* * Selected log table identifier, relies * on the current selected partition via table_log.active_partition */ typedef int TableLogPartitionId; table_log-0.6.4/table_log.md000066400000000000000000000452051446124364700157440ustar00rootroot00000000000000table_log for PostgreSQL ======================== Table of contents: 1. Info 2. License 3. Installation 4. Documentation 4.1. Manual table log and trigger creation 4.2. Restore table data 5. Hints 5.1. Security tips 6. Bugs 7. Todo 8. Changes 9. Contact # 1. Info table_log is a set of functions to log changes on a table in PostgreSQL and to restore the state of the table or a specific row on any time in the past. For now it contains 2 functions: * `table_log()` -- log changes to another table * `table_log_restore_table()` -- restore a table or a specific column NOTE: you can only restore a table where the original table and the logging table has a primary key! This means: you can log everything, but for the restore function you must have a primary key on the original table (and of course a different pkey on the log table). In the beginning (for table_log()) i have used some code from noup.c (you will find this in the contrib directory), but there should be no code left from noup, since i rewrote everything during the development. In fact, it makes no difference since both software is licensed with the BSD style licence which is used by PostgreSQL. # 2. License Copyright (c) 2002-2007 Andreas Scherbaum Basically it's the same as the PostgreSQL license (BSD license). Permission to use, copy, modify, and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of the author not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. The author makes no representations about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty. # 3. Installation Build table_log: ``` make USE_PGXS=1 (and as root) make USE_PGXS=1 install ``` If you are using a version >= 9.1 of PostgreSQL, you can use the new regression tests to verify the working state of the module: ``` USE_PGXS=1 make installcheck ``` Since the regression checks require the extension infrastructure, this won't work on version below 9.1. ## 3.1 Pre-9.1 installation procedure The pg_config tool must be in your $PATH for installation and you must have the PostgreSQL development packages installed. Of course, the usual development tools like make, gcc ect. should also be there ;-) After this you have to create some new functions: - in every database you want to use this functions - if you add this functions to template1, they will be copied to every new database - for older pg versions <= 7.2 change "RETURNS trigger" to "RETURNS opaque", but versions < 7.4 are no longer supported ``` CREATE FUNCTION "table_log_basic" () RETURNS trigger AS '$libdir/table_log', 'table_log' LANGUAGE 'C'; CREATE FUNCTION "table_log" () RETURNS trigger AS '$libdir/table_log', 'table_log' LANGUAGE 'C'; CREATE FUNCTION "table_log_restore_table" (VARCHAR, VARCHAR, CHAR, CHAR, CHAR, TIMESTAMPTZ, CHAR, INT, INT) RETURNS VARCHAR AS '$libdir/table_log', 'table_log_restore_table' LANGUAGE 'C'; CREATE FUNCTION "table_log_restore_table" (VARCHAR, VARCHAR, CHAR, CHAR, CHAR, TIMESTAMPTZ, CHAR, INT) RETURNS VARCHAR AS '$libdir/table_log', 'table_log_restore_table' LANGUAGE 'C'; CREATE FUNCTION "table_log_restore_table" (VARCHAR, VARCHAR, CHAR, CHAR, CHAR, TIMESTAMPTZ, CHAR) RETURNS VARCHAR AS '$libdir/table_log', 'table_log_restore_table' LANGUAGE 'C'; CREATE FUNCTION "table_log_restore_table" (VARCHAR, VARCHAR, CHAR, CHAR, CHAR, TIMESTAMPTZ) RETURNS VARCHAR AS '$libdir/table_log', 'table_log_restore_table' LANGUAGE 'C'; ``` There's also a script available in PATH_TO_YOUR_PGSQL/share/contrib, which contains all required function definitions plus some regression tests, for example: ``` psql -f PATH_TO_YOUR_PGSQL/share/contrib/table_log.sql ``` Install table_log_init() by running `psql < table_log_init.sql`. This function does all the work which is necessary for creating a logging table. NOTE: Currently the Makefile doesn't distinguish between extension and contrib installation procedures and will install table_log--x.x.sql and control files in pre-9.1 versions nevertheless. This might get solved in the future, but don't use those scripts in pre-9.1 installations. ## 3.2 Installation procedure with versions >= 9.1 Starting with PostgreSQL 9.1 you should use the new extension infrastructure. table_log supports this starting with version 0.5. After compiling and installing the module, all you need to do is to connect to the target database and issue the following command: ``` CREATE EXTENSION table_log; ``` This will install all functions including table_log_init() into the public schema. NOTE: The old contrib scripts are going to be installed, still. We currently don't have distinguished the installation procedure version-wise within the Makefile, so don't get confused if you still find the old table_log.sql scripts in share/extension directories. ## 3.3 Upgrading from earlier installations to the new extension infrastructure If you are upgrading from an earlier pre-9.1 installation, and you want to use the new extension infrastructure, you could use the unpackaged creation procedure to migrate an existing table_log installation within a database into an extension. To accomplish this, you need to execute the following command in the new 9.1+ database (of course, after installing the new version of table_log): ``` CREATE EXTENSION table_log FROM unpackaged; ``` # 4. Documentation The entire log table and trigger creation can be done using the table_log_init(ncols, ...) function. The parameter ncols decides how many extra columns will be added to the created log table, it can be 3, 4 or 5. The extra columns are described in chapter 4.1. The function can be used with the following parameters: table_log_init(ncols, tablename): create the log table as tablename_log table_log_init(ncols, tablename, logschema): create the log table with the same name as the original table, but in the schema logschema. table_log_init(ncols, tableschema, tablename, logschema, logname): log the changes in table tableschema.tablename into the log table logschema.logname. table_log_init(ncols, tableschema, tablename, logschema, logname, partition_mode, basic_mode, log_actions): log the changes in table tableschema.tablename into the log table logschema.logname. The parameter partition_mode can be SINGLE or PARTITION, which creates two log tables *_0 and *_1 which can be switched by setting table_log.active_partition to the corresponding partition id 1 or 2. basic_mode defines wether we use full logging mode or basic mode. When set to TRUE, table_log_basic() will be used internally which suppresses logging of NEW values by UPDATE actions. log_actions is a TEXT[] array which specifies a list of either INSERT, DELETE or UPDATE or any combinations from them to tell when a table_log trigger should be fired. NOTE: When using basic_mode and/or log_actions without all actions, you won't be able to use table_log_restore_table() anymore. When calling table_log_init(), you can omit logname in any case. The function will then generate a log tablename from the given source tablename and a `_log` string appended. ## 4.1. Manual table log and trigger creation Create an trigger on a table with log table name as argument. If no table name is given, the actual table name plus `_log` will be used by table_log. Example: ``` CREATE TRIGGER test_log_chg AFTER UPDATE OR INSERT OR DELETE ON test_table FOR EACH ROW EXECUTE PROCEDURE table_log(); ^^^^^ 'test_table_log' will be used to log changes CREATE TRIGGER test_log_chg AFTER UPDATE OR INSERT OR DELETE ON test_table FOR EACH ROW EXECUTE PROCEDURE table_log('log_table'); ^^^^^ 'log_table' will be used to log changes ``` The log table needs exact the same columns as the original table (but without any constraints) plus three, four or five extra columns: ``` trigger_mode VARCHAR(10) trigger_tuple VARCHAR(5) trigger_changed TIMESTAMPTZ trigger_user VARCHAR(32) -- optional trigger_mode contains 'INSERT', 'UPDATE' or 'DELETE' trigger_tuple contains 'old' or 'new' trigger_changed is the actual timestamp inside the trancaction (or maybe i should use here the actual system timestamp?) trigger_user contains the session user name (the one who connected to the database, this must not be the actual one) ``` On INSERT, a log entry with the 'new' tuple will be written. On DELETE, a log entry with the 'old' tuple will be written. On UPDATE, a log entry with the old tuple and a log entry with the new tuple will be written. A fourth column is possible on the log table: trigger_id BIGINT contains an unique id for sorting table log entries NOTE: for the restore function you must have this 4. column! Q: Why do i need this column? A: Because we have to sort the logs to get them back in correct order. Hint: if you are sure, you don't have an OID wrapover yet, you can use the OID column as unique id (but if you have an OID wrapover later, the new OIDs doesn't follow a linear scheme, see VACUUM documentation) A fifth column is possible on the log table: trigger_user VARCHAR(32) contains the username from the user who originally opened the database connection Q: Why not the actual user? A: If someone changed the actual user with a setuid function, you always know the original username Q: Why 32 bytes? A: The function doesn't uses 32 bytes but instead NAMEDATALEN defined at compile time (which defaults to 32 bytes) Q: May i skip the log table name? A: No, because Pg then thinks, the '1' is the first parameter and will fail to use '1' as logging table This is an backwards compatibility issue, sorry for this. For backward compatibility table_log() works with 3, 4 or 5 extra columns, but you should use the 4 or 5 column version everytimes. A good method to create the log table is to use the existing table: ``` -- create the table without data SELECT * INTO test_log FROM test LIMIT 0; ALTER TABLE test_log ADD COLUMN trigger_mode VARCHAR(10); ALTER TABLE test_log ADD COLUMN trigger_tuple VARCHAR(5); ALTER TABLE test_log ADD COLUMN trigger_changed TIMESTAMPTZ; -- now activate the history function CREATE TRIGGER test_log_chg AFTER UPDATE OR INSERT OR DELETE ON test FOR EACH ROW EXECUTE PROCEDURE table_log(); -- or the 4 column method: SELECT * INTO test_log FROM test LIMIT 0; ALTER TABLE test_log ADD COLUMN trigger_mode VARCHAR(10); ALTER TABLE test_log ADD COLUMN trigger_tuple VARCHAR(5); ALTER TABLE test_log ADD COLUMN trigger_changed TIMESTAMPTZ; ALTER TABLE test_log ADD COLUMN trigger_id BIGINT; CREATE SEQUENCE test_log_id; SELECT SETVAL('test_log_id', 1, FALSE); ALTER TABLE test_log ALTER COLUMN trigger_id SET DEFAULT NEXTVAL('test_log_id'); -- now activate the history function CREATE TRIGGER test_log_chg AFTER UPDATE OR INSERT OR DELETE ON test FOR EACH ROW EXECUTE PROCEDURE table_log(); -- or the 5 column method (with user name in 'trigger_user'): SELECT * INTO test_log FROM test LIMIT 0; ALTER TABLE test_log ADD COLUMN trigger_mode VARCHAR(10); ALTER TABLE test_log ADD COLUMN trigger_tuple VARCHAR(5); ALTER TABLE test_log ADD COLUMN trigger_changed TIMESTAMPTZ; ALTER TABLE test_log ADD COLUMN trigger_user VARCHAR(32); ALTER TABLE test_log ADD COLUMN trigger_id BIGINT; CREATE SEQUENCE test_log_id; SELECT SETVAL('test_log_id', 1, FALSE); ALTER TABLE test_log ALTER COLUMN trigger_id SET DEFAULT NEXTVAL('test_log_id'); -- now activate the history function -- you have to give the log table name! CREATE TRIGGER test_log_chg AFTER UPDATE OR INSERT OR DELETE ON test FOR EACH ROW EXECUTE PROCEDURE table_log('test_log', 1); ``` See table_log.sql for a demo ## 4.2. Restore table data Now insert, update and delete some data in table 'test'. After this, you may want to restore your table data: ``` SELECT table_log_restore_table(, , , , , , , , ); ``` The parameter list means: - original table name: string The name of the original table (test in your example above) - original table primary key: string The primary key name of the original table - log table name: string The name of the logging table - log table primary key: string The primary key of the logging table (trigger_id in your example above) Note: this cannot be the same as the original table pkey! - restore table name: string The name for the restore table Note: this table must not exist! Also see - timestamp: timestamp The timestamp in past Note: if you give a timestamp where no logging data exists, absolutly nothing will happen. But see - primary key to restore: string (or NULL) If you want to restore only a single primary key, name it here. Then only data for this pkey will be searched and restored Note: this parameter is optional and defaults to NULL (restore all pkeys) you can say NULL here, if you want to skip this parameter - restore method: 0/1 (or NULL) 0 means: first create the restore table and then restore forward from the beginning of the log table 1 means: first create the log table and copy the actual content of the original table into the log table, then restore backwards Note: this can speed up things, if you know, that your timestamp point is near the end or the beginning Note: this parameter is optional and defaults to NULL (= 0) - dont create temporary table: 0/1 (or NULL) Normal the restore table will be created temporarly, this means, the table is only available inside your session and will be deleted, if your session (session means connection, not transaction) is closed This parameter allows you to create a normal table instead Note: if you want to use the restore function sometimes inside a session and you want to use the same restore table name again, you have to drop the restore table or the restore function will blame you Note: this parameter is optional and defaults to NULL (= 0) # 5. Hints - an index on the log table primary key (trigger_id) and the trigger_changed column will speed up things - You can find another nice explanation in my blog: http://ads.wars-nicht.de/blog/archives/100-Log-Table-Changes-in-PostgreSQL-with-tablelog.html ## 5.1. Security Tips - You can create the table_log functions with 'SECURITY DEFINER' privileges and revoke the permissions for the logging table for any normal user. This allows you to have an audit which cannot be modified by users who are accessing the data table. - Existing functions can be modified with: ALTER FUNCTION table_log() SECURITY DEFINER; # 6. Bugs - nothing known, but tell me, if you find one # 7. Todo - table_log_show_column() allows select of previous state (possible with PostgreSQL 7.3 and higher) see Table Function API - is it binary safe? (\000) - do not only check the number columns in both tables, really check the names of the columns # 8. Changes: - 2002-04-06: Andreas 'ads' Scherbaum (ads@ufp.de) first release - 2002-04-25: Steve Head (smhf@onthe.net.au) there was a bug with NULL values, thanks to Steve Head for reporting this. - 2002-04-25: Andreas 'ads' Scherbaum (ads@ufp.de) now using version numbers (0.0.5 for this release) - 2002-09-09: Andreas 'ads' Scherbaum (ads@ufp.de) fix bug in calculating log table name release 0.0.6 - 2003-03-22: Andreas 'ads' Scherbaum (ads@ufp.de) fix some error messages (old name from 'noup' renamed to 'table_log') one additional check that the trigger is fired after release 0.0.7 - 2003-03-23: Andreas 'ads' Scherbaum (ads@ufp.de) create a second Makefile for installing from source release 0.1.0 - 2003-04-20: Andreas 'ads' Scherbaum (ads@ufp.de) change Makefile.nocontrib to Linux only and make a comment about installation informations for other platforms (its too difficult to have all install options here, i dont have the ability to test all platforms) - 2003-06-12: Andreas 'ads' Scherbaum (ads@ufp.de) update documentation (thanks to Erik Thiele for pointing this out) - 2003-06-13: Andreas 'ads' Scherbaum (ads@ufp.de) - release 0.2.0 now allow 3 or 4 columns update documentation about trigger_id column - 2003-06-13: Andreas 'ads' Scherbaum (ads@ufp.de) - release 0.2.1 add debugging (activate TABLE_LOG_DEBUG in head of table_log.c and recompile) - 2003-11-27: Andreas 'ads' Scherbaum (ads@ufp.de) - release 0.3.0 add function for restoring table from log cleanup source add more debugging - 2003-12-11: Andreas 'ads' Scherbaum (ads@ufp.de) - release 0.3.1 add session_user to log table on request thanks to iago@patela.org.uk for the feature request fix a minor bug in returning the table name - 2005-01-14: Andreas 'ads' Scherbaum (ads@wars-nicht.de) - release 0.4.0 ignore dropped columns on tables (this may cause errors, if you restore or use older backups) change email address, the old one does no longer work - 2005-01-24: Andreas 'ads' Scherbaum (ads@wars-nicht.de) - release 0.4.1 there seems to be an problem with session_user - 2005-04-22: Kim Hansen - release 0.4.2 added table_log_init() added schema support - 2006-08-30: Andreas 'ads' Scherbaum (ads@wars-nicht.de) - release 0.4.3 drop support for < 7.4 (return type is TRIGGER now) fix bug with dropped columns - 2007-05-18: Andreas 'ads' Scherbaum (ads@pgug.de) - release 0.4.4 compatibility issues with 8.2.x some small fixes in table_log.sql.in remove some warnings docu cleanups Thanks to Michael Graziano for pointing out the 8.2 fix Thanks to Alexander Wirt for Debian packaging Thanks to Devrim GÜNDÜZ for RPM packaging # 9. Contact The project is hosted at http://pgfoundry.org/projects/tablelog/ If you have any hints, changes or improvements, please contact me. my gpg key: pub 1024D/4813B5FE 2000-09-29 Andreas Scherbaum Key fingerprint = 9F67 73D3 43AA B30E CA8F 56E5 3002 8D24 4813 B5FE table_log-0.6.4/table_log.sql.in000066400000000000000000000044561446124364700165530ustar00rootroot00000000000000-- -- table_log () -- log changes to another table -- -- -- see README.md for details -- -- -- written by Andreas ' ads' Scherbaum (ads@pgug.de) -- -- -- drop old trigger DROP TRIGGER test_log_chg ON test; -- ignore any error -- create demo table DROP TABLE test; -- ignore any error CREATE TABLE test ( id INT NOT NULL PRIMARY KEY, name VARCHAR(20) NOT NULL ); -- create the table without data from demo table DROP TABLE test_log; -- ignore any error SELECT * INTO test_log FROM test LIMIT 0; ALTER TABLE test_log ADD COLUMN trigger_mode VARCHAR(10); ALTER TABLE test_log ADD COLUMN trigger_tuple VARCHAR(5); ALTER TABLE test_log ADD COLUMN trigger_changed TIMESTAMPTZ; ALTER TABLE test_log ADD COLUMN trigger_id BIGINT; CREATE SEQUENCE test_log_id; SELECT SETVAL('test_log_id', 1, FALSE); ALTER TABLE test_log ALTER COLUMN trigger_id SET DEFAULT NEXTVAL('test_log_id'); -- create function CREATE FUNCTION table_log () RETURNS TRIGGER AS 'MODULE_PATHNAME' LANGUAGE C; CREATE FUNCTION "table_log_restore_table" (VARCHAR, VARCHAR, CHAR, CHAR, CHAR, TIMESTAMPTZ, CHAR, INT, INT) RETURNS VARCHAR AS 'MODULE_PATHNAME', 'table_log_restore_table' LANGUAGE C; CREATE FUNCTION "table_log_restore_table" (VARCHAR, VARCHAR, CHAR, CHAR, CHAR, TIMESTAMPTZ, CHAR, INT) RETURNS VARCHAR AS 'MODULE_PATHNAME', 'table_log_restore_table' LANGUAGE C; CREATE FUNCTION "table_log_restore_table" (VARCHAR, VARCHAR, CHAR, CHAR, CHAR, TIMESTAMPTZ, CHAR) RETURNS VARCHAR AS 'MODULE_PATHNAME', 'table_log_restore_table' LANGUAGE C; CREATE FUNCTION "table_log_restore_table" (VARCHAR, VARCHAR, CHAR, CHAR, CHAR, TIMESTAMPTZ) RETURNS VARCHAR AS 'MODULE_PATHNAME', 'table_log_restore_table' LANGUAGE C; -- create trigger CREATE TRIGGER test_log_chg AFTER UPDATE OR INSERT OR DELETE ON test FOR EACH ROW EXECUTE PROCEDURE table_log(); -- test trigger INSERT INTO test VALUES (1, 'name'); SELECT * FROM test; SELECT * FROM test_log; UPDATE test SET name='other name' WHERE id=1; SELECT * FROM test; SELECT * FROM test_log; -- create restore table SELECT table_log_restore_table('test', 'id', 'test_log', 'trigger_id', 'test_recover', NOW()); SELECT * FROM test_recover; DROP TABLE test_log; DROP TABLE test; table_log-0.6.4/tests/000077500000000000000000000000001446124364700146265ustar00rootroot00000000000000table_log-0.6.4/tests/dropped_column.sql000066400000000000000000000014331446124364700203620ustar00rootroot00000000000000BEGIN; CREATE TABLE drop_test ( id SERIAL NOT NULL PRIMARY KEY, col1 VARCHAR(20) NOT NULL DEFAULT '', col2 VARCHAR(20) NOT NULL DEFAULT '', col3 VARCHAR(20) NOT NULL DEFAULT '' ); -- init tablelog SELECT table_log_init(5, 'public', 'drop_test', 'public', 'drop_test_log'); INSERT INTO drop_test (col1, col2, col3) VALUES ('a1', 'b1', 'c1'); SELECT * FROM drop_test; SELECT * FROM drop_test_log; ALTER TABLE drop_test DROP COLUMN col2; ALTER TABLE drop_test_log DROP COLUMN col2; INSERT INTO drop_test (col1, col3) VALUES ('a2', 'c2'); SELECT * FROM drop_test; SELECT * FROM drop_test_log; ROLLBACK; table_log-0.6.4/uninstall_table_log.sql.in000066400000000000000000000007431446124364700206370ustar00rootroot00000000000000BEGIN; -- drop old function DROP FUNCTION table_log (); -- ignore any error DROP FUNCTION "table_log_restore_table" (VARCHAR, VARCHAR, CHAR, CHAR, CHAR, TIMESTAMPTZ, CHAR, INT, INT); DROP FUNCTION "table_log_restore_table" (VARCHAR, VARCHAR, CHAR, CHAR, CHAR, TIMESTAMPTZ, CHAR, INT); DROP FUNCTION "table_log_restore_table" (VARCHAR, VARCHAR, CHAR, CHAR, CHAR, TIMESTAMPTZ, CHAR); DROP FUNCTION "table_log_restore_table" (VARCHAR, VARCHAR, CHAR, CHAR, CHAR, TIMESTAMPTZ); COMMIT;