Data Modification Tracking
ProvSQL can track the provenance of data-modification operations –
INSERT, UPDATE, and DELETE – when run on provenance-enabled
tables [Bourhis et al., 2020].
Note
Data modification tracking requires PostgreSQL ≥ 14.
Enabling Update Provenance
Update-provenance tracking is disabled by default. Enable it for a session:
SET provsql.update_provenance = on;
Or permanently in postgresql.conf:
provsql.update_provenance = on
INSERT
Inserting a row into a provenance-enabled table always creates a new
input gate for that row. When update_provenance is enabled, the
statement additionally creates an update gate, logged in the
update_provenance table, that is multiplied into each inserted
row’s provenance – so the insertion as a whole can later be undone:
INSERT INTO employees(name, dept)
VALUES ('Alice', 'R&D');
-- The new row already has a provenance token
SELECT name, provenance() FROM employees WHERE name = 'Alice';
DELETE
Deleting a row does not remove it from the table, but the provenance is changed to mark the deletion, allowing hypothetical reasoning.
The
undo mechanism (see below) relies on this.
DELETE FROM employees WHERE name = 'Alice';
UPDATE
An UPDATE is modelled as a DELETE followed by an INSERT. The
new row gets a fresh provenance token; the old token continues to exist in
the circuit.
UPDATE employees SET dept = 'Sales' WHERE name = 'Bob';
Undoing Updates
ProvSQL provides an undo function that rolls back the
provenance effects of a specific logged modification. Every
provenance-enabled DML statement is recorded in the update_provenance
table; pass its provsql token to undo to reverse its effect:
CREATE TABLE t(id INT PRIMARY KEY);
SELECT add_provenance('t');
INSERT INTO t VALUES (1), (2), (3);
DELETE FROM t WHERE id = 3;
-- Row 3's provenance is now zeroed; undo the DELETE to restore it
SELECT undo(provsql)
FROM update_provenance
WHERE query = 'DELETE FROM t WHERE id = 3;';
Transactions
Each statement mints an update gate of its own, and every statement of
one transaction also names the transaction’s gate
(transaction_token): what a statement does to a row is recorded
as times(transaction, statement). So the
update_provenance table says which statements were one transaction –
its tx_token column names the transaction’s gate and its xid
column the PostgreSQL transaction id – and undo works at
either granularity:
BEGIN;
DELETE FROM t WHERE id = 1;
UPDATE t SET v = 'B' WHERE id = 2;
COMMIT;
-- Reverse the whole transaction, both statements at once
SELECT undo(provsql)
FROM update_provenance
WHERE query_type = 'TRANSACTION'
ORDER BY ts DESC LIMIT 1;
A transaction’s own row has no query text – a transaction is not a
statement – so looking a statement up by its text finds the statement and
not the transaction that carried it. Its validity is the universal range,
the identity of the temporal semiring: it multiplies into every effect of
the transaction, so anything narrower would intersect itself into all of
them. When the transaction rolls back, nothing of it remains: the log
rows and the token rewrites are ordinary heap writes.
ts and the start of valid_time are stamped at commit, not when
the statement ran. CURRENT_TIMESTAMP is the transaction’s start time,
so two overlapping transactions could otherwise commit in the opposite
order of the validity they recorded; a deferred trigger on
update_provenance moves both to the commit instant.
The two notions of undo remain distinct and consistent: PostgreSQL’s
ROLLBACK removes a modification and its record – the transaction
never happened – while undo appends a compensating update
gate and keeps the history: the modification happened and was reversed.
Limitations
Update tracking is still experimental, both in terms of operation support and of performance.
The “deleted rows stay in the table” model is implemented as a physical
delete followed by a re-insert carrying the monus token. Under
READ COMMITTED, a concurrent transaction blocked on the same row
therefore sees, once the first commits, the original row version gone and
the re-inserted copy invisible to its snapshot: its own DELETE
affects zero rows but still fires the statement trigger, logging an
update gate that touches nothing. Under REPEATABLE READ it gets a
serialization failure instead. This is ordinary PostgreSQL behaviour for
a row rewritten under a concurrent reader, but it is worth knowing.