Skip to content
This project is part of pgEdge Labs and is under active development. APIs and features may change without notice.

Undoing Changes

This document describes every way to select what pgVolvra reverts, and what pgVolvra does when a row has changed since the mistake. Previewing an undo is always safe; applying one requires an explicit confirmation.

Previewing before applying

volvra.preview_undo returns the plan and executes nothing:

SELECT seq, table_name, op, inverse_op, pk, conflict, stmt
FROM volvra.preview_undo('orders', now() - interval '10 minutes');

The stmt column holds the exact statement pgVolvra would run, so the preview is auditable. volvra.undo without confirm => true behaves the same way.

Applying an undo

Add confirm => true to apply the plan:

SELECT * FROM volvra.undo('orders', now() - interval '10 minutes',
                          now(), confirm => true);

pgVolvra applies the whole plan inside the caller's transaction, so the undo commits completely or not at all. pgVolvra captures the undo, so an undo can itself be undone.

Selecting what to revert

volvra.preview_undo and volvra.undo take the same selector. At least one criterion is required, because planning an undo across every covered table by accident should not be possible.

The following table describes each selector argument:

Argument Selects
target One table.
tables Several tables, as an array of regclass.
from_ts Changes captured after this timestamp.
to_ts Changes captured up to and including this timestamp.
txid One transaction, across every table the transaction touched.
actor Changes an application declared it made.
db_user Changes one authenticated role made.
predicate A SQL boolean over old_row, new_row, pk, actor, db_user, ts, and txid.

Combine the arguments freely. The following statement reverts one service's damage, limited to one customer's rows, within the last hour:

SELECT * FROM volvra.undo(
  'orders', now() - interval '1 hour', now(),
  confirm   => true,
  actor     => 'svc:pricing',
  predicate => $$old_row->>'customer' = 'acme'$$);

Reverting a whole transaction

A mistaken migration is one transaction across several tables, which is the unit people remember. Find the transaction, then revert it:

SELECT txid, ended, db_users, tables, changes FROM volvra.transactions();
SELECT * FROM volvra.undo_txid(848291, confirm => true);

volvra.undo_txid and volvra.preview_undo_txid are wrappers around the selector, so the behavior is identical to passing txid.

Marking a moment to come back to

A mark is a named moment. Take one before a deployment, and undoing to it puts the covered tables back the way they were:

SELECT volvra.mark('before-deploy', 'release 042');
-- ... the deployment goes wrong ...
SELECT * FROM volvra.undo_to('before-deploy', confirm => true);

List the marks with what each would cost, before committing to one:

SELECT name, age, changes_since, tables_since, note FROM volvra.marks();

The changes_since column is the point of the function. Undoing to a mark reverts everything recorded since, so knowing the size of that first is the difference between a considered decision and a guess.

A duplicate name raises an error rather than moving the existing mark; pass p_replace => true to move one deliberately. Removing a mark with volvra.unmark removes a pointer and never the history.

A mark is blunter than a transaction

Undoing to a mark reverts every recorded change since that moment, on every covered table in scope. That includes changes other people made legitimately, because those changes are inside the window rather than outside it, so the conflict guard has no reason to object.

The following table compares the two ways of scoping an undo:

Scope Answers Use when
A mark Put these tables back the way they were. You want a point-in-time restore of covered tables.
A transaction id Undo that specific mistake. Someone else may have done legitimate work since.

Prefer a transaction id in production. Reach for a mark when the intent really is to rewind, such as a failed deployment on a database nobody else is using.

The conflict guard still applies to a mark, but it only fires for a change pgVolvra never recorded, such as one made while capture was disabled. Anything pgVolvra did record since the mark is part of what you asked to revert.

Using a predicate safely

The predicate is a WHERE fragment, not a statement. pgVolvra parenthesizes the fragment before splicing the fragment into the query, and refuses any predicate containing a semicolon, a double hyphen, or a slash-star comment opener.

The predicate runs with the caller's own privileges, so the predicate is a filter rather than a privilege escalation.

When a row has changed since the mistake

Every compensating statement carries a guard that matches only if the live row still holds the captured values. pgVolvra refuses the whole undo when any row has moved on, and raises serialization_failure.

Revert everything else and leave the changed rows alone by passing skip_conflicts:

SELECT seq, pk, status
FROM volvra.undo('orders', :t0, :t1,
                 confirm => true, skip_conflicts => true);

The status column reports applied or skipped for each row. pgVolvra offers no option to overwrite a row that has changed, because overwriting is the data loss the guard exists to prevent.

The guard tests only the columns the undo writes. A later change to a different column is not a conflict, because reverting these columns cannot destroy that change.

Limiting the blast radius

pgVolvra refuses an undo that would affect more rows than max_undo_rows, and raises program_limit_exceeded. Override the cap for a single call:

SELECT * FROM volvra.undo('orders', :t0, :t1,
                          confirm => true, max_rows => 250000);

pgVolvra records the override in volvra.undo_log, so an unusually large undo is visible afterwards.

pgVolvra walks a plan in reverse chronological order across every selected table. For ordinary foreign keys that order is already correct, because the application could only have deleted a child before its parent.

An ON DELETE CASCADE is the exception. A cascade records the parent first and the children afterwards, so reversing the cascade tries to resurrect a child before its parent. Make the constraints deferrable once, as an administrator:

SELECT constraint_name, table_name, status
FROM volvra.make_fks_deferrable('public');

DEFERRABLE INITIALLY IMMEDIATE does not change day to day behavior; constraints are still checked at the end of each statement. The change only allows an undo to defer the checks to COMMIT inside its own transaction. The command takes a brief ACCESS EXCLUSIVE lock per table, so run the command in a maintenance window.

Without deferrable constraints, an undo that trips a foreign key fails with foreign_key_violation, names this function, and applies nothing.

When an undo finds nothing

pgVolvra distinguishes three situations, because "no changes reverted" and "no record of this table" call for different responses. The following table describes each case:

Situation Behavior
The table was never covered pgVolvra raises invalid_parameter_value and explains that no record exists.
The table was covered in the past pgVolvra reverts the recorded history and reports a notice that coverage has stopped.
The table is covered but the window is empty pgVolvra reports zero changes, which is not an error.

Errors an undo can raise

The following table describes each error and its cause:

Condition Cause
serialization_failure A row changed after the mistake and skip_conflicts is false.
program_limit_exceeded The plan affects more rows than max_undo_rows allows.
foreign_key_violation The plan spans related tables and a constraint is not deferrable.
datatype_mismatch The table changed shape, or a required column is excluded from capture.
data_exception The selection crosses a TRUNCATE whose rows were not captured, or a captured change carries no restorable column.
invalid_parameter_value The target table has never been covered.
null_value_not_allowed No selector was given.
insufficient_privilege The caller lacks the required role, or write privileges on the target.

Next Steps

  • The Viewing History document describes how to inspect a row's history before reverting.
  • The Covering Tables document explains coverage.
  • The CLI Reference document documents the command line equivalent of each operation.