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

Architecture

This document explains how pgVolvra records changes and how pgVolvra reverts them. Understanding the capture path helps you predict what pgVolvra costs and what pgVolvra can recover.

Capturing changes

The trigger tier attaches two triggers to each covered table. A row-level AFTER trigger records every INSERT, UPDATE, and DELETE. A statement-level BEFORE trigger handles TRUNCATE, which row triggers never see.

Each captured change becomes one row in volvra.change_log. The following table describes what pgVolvra stores for each operation:

Operation Stored
INSERT The new row image.
UPDATE The old and new values of the columns that changed.
DELETE The complete old row image.
TRUNCATE One delete image per row, when on_truncate is capture (the default).

pgVolvra stores row images as jsonb. An UPDATE stores only the columns whose values differ, because the primary key travels in its own column and nothing else is needed to find the row again. An UPDATE that changed nothing records nothing at all.

Nothing leaves the database. pgVolvra adds one insert into a local append-only table inside the transaction that made the change. pgVolvra does not duplicate statements, open additional connections, or write outside the database.

Reverting changes

An undo is a plan, and the plan is ordinary SQL. pgVolvra selects the captured changes you asked for, walks them in reverse chronological order, and generates a compensating statement for each one.

The following table describes the compensating statement pgVolvra generates for each captured operation:

Captured operation Compensating statement
INSERT DELETE the row by primary key.
DELETE INSERT the stored old row image.
UPDATE UPDATE the captured columns back to their old values.

pgVolvra applies the plan inside the caller's transaction, so the whole undo commits or none of it does. An undo makes ordinary changes to covered tables, so pgVolvra captures the undo as well; an undo can therefore be undone.

The conflict guard

Every compensating statement carries a guard that matches only if the live row still holds the values pgVolvra captured. If someone changed the row after the mistake, the statement affects no rows and pgVolvra refuses the entire undo.

The guard tests only the columns the undo is about to write. If a mistake set total to zero and someone later edited note, pgVolvra still reverts total, because reverting total cannot destroy an edit to note. A later change to total itself does conflict.

pgVolvra offers two outcomes for a conflict: refusing the undo, which is the default, and skipping the conflicting rows while reverting the rest. pgVolvra deliberately offers no option to overwrite a row that has moved on.

Storage layout

pgVolvra creates one schema, named volvra, containing the history and its supporting objects. The following table describes the principal tables:

Table Contents
change_log Captured row images, partitioned by month.
enabled_tables Covered tables, their primary keys, and per-table settings.
undo_log Every undo attempt, previewed or applied.
seal Hash chain proving the history has not been altered.
settings Configuration values.
retention Per-table retention policy.
retention_log Every retention run and the row ranges removed.
erasure_log Every erasure request and what it removed.
companion_checkpoint Progress reported by the companion.
companion_gap History the companion could not archive.
schema_version Migrations applied to this database.

The change_log table is range-partitioned by month, so reclaiming old history drops a partition rather than deleting rows through the append-only guard.

The durable tier

The trigger tier lives inside the database and therefore shares the fate of the database. The companion is a separate process that reads a logical replication slot and writes change data to storage you own.

The companion decodes with pgoutput, the only logical decoding plugin built into PostgreSQL. Plugins such as wal2json are server-side extensions, which managed providers do not offer.

The companion writes newline-delimited JSON in numbered segments, described by a manifest that chains the SHA-256 hash of each segment. The archive is readable without the companion and without PostgreSQL. See the Companion Overview document.

Next Steps

  • The Getting Started document walks through a first undo.
  • The Performance document gives measured throughput and storage costs.
  • The Security document describes the privilege model.