Skip to main content

Branching

By the end of this page you will be able to fork a ScramDB database instantly, connect to the fork like any other database, and use that fork for testing, debugging, CI, and agent sandboxes.

Branching is ScramDB's "give me a working copy of the database right now" feature. It runs entirely in SQL, against a live, running server, and it is instant because it does not copy data: a branch is a copy-on-write fork that shares storage with its parent at the moment it is created.

The two forms​

CLONE: fork the current state​

CREATE DATABASE staging_test CLONE scramdb;

Instant, present-state, copy-on-write fork of scramdb into a new database called staging_test. It needs zero extra configuration, and works out of the box on a single node and on a cluster (see Branching on a cluster).

PostgreSQL-familiar spellings work identically, they are accepted synonyms rewritten to the same operation:

CREATE DATABASE staging_test2 TEMPLATE scramdb;
CREATE DATABASE staging_test3 TEMPLATE = scramdb; -- the = form is equivalent

FROM ... AT TIMESTAMP: fork a past state​

CREATE DATABASE audit_before_incident FROM scramdb AT TIMESTAMP '2026-08-02T09:30:00Z';

Forks scramdb as it existed at the given timestamp, rather than as it exists right now. This requires [storage.wal.backup].destination to be configured and a base backup to already exist there, plus WAL history back to your target timestamp, either in the local WAL directory or the WAL archive. See Backups to set that up if you have not already; if the config key is missing, the statement fails loud naming it directly, it does not fail silently or produce an empty database.

The timestamp must be an RFC3339 literal, e.g. '2026-08-02T09:30:00Z' or '2026-08-02T09:30:00.123456Z' for microsecond precision.

There is also a function-call form, equivalent to the SQL syntax above:

SELECT scram.branch_at('scramdb', 'audit_before_incident', '2026-08-02T09:30:00Z');

A bare CREATE DATABASE child FROM parent; with no AT TIMESTAMP clause is not valid syntax, it is a hard parse error. FROM is only ever followed by AT TIMESTAMP '<literal>'.

What copy-on-write means here​

At the moment of the fork, the child database shares its parent's underlying storage: nothing is physically copied, which is why the fork completes instantly regardless of how large the parent database is. From that point on, the two databases are fully independent. A write to the parent after the fork never appears in the child, and a write to the child never appears in the parent; each side only consumes new storage for the data it actually changes. You get an isolated, full-sized working copy at the storage cost of the changes you make to it, not the size of the whole database.

Branching on a cluster​

Both forms work on a cluster, issued through any node, and the branch opens on every node.

A CLONE forks the parent at one commit position that every shard group of the parent shares: every commit at or below it is in the branch and none after it, so a transaction that wrote several groups is in the branch on all of them or on none. Each group keeps the row versions the fork needs until its replicas have forked the branch, and the metadata group decides the clone once. A CLONE needs a quorum of each shard group, never every member; a node that was down takes the fork from the group's log when it returns. A clone that cannot give every shard group its fork is dropped again on every node, and the statement says why.

A branch at a past time (FROM ... AT TIMESTAMP or scram.branch_at) reads the cluster's WAL archive instead of a base backup: turn on [storage.wal.archive] on every node, pointed at one shared destination, as a cluster restore needs. The time resolves to one commit position every group shares, exactly as a cluster restore's --target-time does; the parent's tables as they were then become the branch's; and each shard group's archived history up to that position is replayed into the branch's own groups, so every replica holds the branch's rows. A table that lives on one node only is brought to that time from that node's own base backup and WAL, as on a single node.

The scramdb_cluster_branch_* metrics on Observability count the branches made, the tables forked and the rows read from the archive.

Connecting to a branch​

A branch is a real, independently-connectable database, reachable by name over the normal PostgreSQL wire protocol, exactly like any other database on the instance. It is not a special mode of the parent's connection, and it does not require a restart, tables created by the branch are visible immediately.

psql "host=127.0.0.1 port=5432 user=scramdb dbname=staging_test"

Any PostgreSQL-compatible driver works the same way, point its dbname (or equivalent connection parameter) at the branch's name.

Cleaning up a branch​

DROP DATABASE staging_test;

This works even if the branch has tables and data in it, DROP DATABASE tears down every table in the branch first, releasing storage the branch changed while leaving data still shared with the parent untouched. DROP DATABASE IF EXISTS staging_test; is also available if you are not sure the branch still exists.

The default database of an instance can never be dropped; ScramDB refuses that outright.

Worked workflows​

1. Testing against real data​

Clone production, try something risky against the clone, throw it away.

CREATE DATABASE migration_test CLONE scramdb;
psql "host=127.0.0.1 port=5432 user=scramdb dbname=migration_test" \
-c "ALTER TABLE orders ADD COLUMN priority integer DEFAULT 0;" \
-c "UPDATE orders SET priority = 1 WHERE status = 'urgent';"

Verify the migration did what you expected against real data volumes and real data shapes:

psql "host=127.0.0.1 port=5432 user=scramdb dbname=migration_test" \
-c "SELECT priority, count(*) FROM orders GROUP BY priority;"
DROP DATABASE migration_test;

Production was never touched, and you tested against the actual data, not a synthetic fixture.

2. Debugging production data​

Branch as of just before an incident, then compare it against the current state to see exactly what changed.

CREATE DATABASE audit_before_incident FROM scramdb AT TIMESTAMP '2026-08-02T09:30:00Z';
psql "host=127.0.0.1 port=5432 user=scramdb dbname=audit_before_incident" \
-c "SELECT id, status FROM orders WHERE id = 101;"
psql "host=127.0.0.1 port=5432 user=scramdb dbname=scramdb" \
-c "SELECT id, status FROM orders WHERE id = 101;"

Comparing the two outputs shows you exactly what the row looked like right before the incident versus now, without touching production and without a separate restore-to-a-new-server round trip.

DROP DATABASE audit_before_incident;

3. CI​

Give every test run its own isolated copy of a known-good fixture database, with no per-run data setup cost.

RUN_ID=$(date +%s)
psql "host=127.0.0.1 port=5432 user=scramdb dbname=scramdb" \
-c "CREATE DATABASE ci_run_${RUN_ID} CLONE template_db;"

# run your test suite against dbname=ci_run_${RUN_ID}

psql "host=127.0.0.1 port=5432 user=scramdb dbname=scramdb" \
-c "DROP DATABASE ci_run_${RUN_ID};"

Every run starts from the exact same fixture state, instantly, and cleans up after itself. No shared mutable test database, no fixture-loading step slowing down the pipeline.

4. Agent sandboxes​

Give each autonomous job or agent its own branch to read and write freely, fully isolated from production and from every other agent's branch.

CREATE DATABASE agent_task_4471 CLONE scramdb;

The agent connects to agent_task_4471 and can run arbitrary reads and writes, including ones you would never let it run against production, since nothing it does there can reach the parent database or any other agent's branch.

DROP DATABASE agent_task_4471;

Known constraints​

  • A branch at a past time on a cluster needs the cluster's archive. Without [storage.wal.archive] on every node, pointed at one shared destination, the archive does not reach the time and the statement says so. See Branching on a cluster.
  • Not inside an explicit transaction block. CREATE DATABASE (either form) is refused if you run it inside BEGIN ... COMMIT. Run it as its own statement.
  • The default database can never be cloned away or dropped. Every instance's default database is protected from DROP DATABASE, regardless of how many branches exist.
  • FROM ... AT TIMESTAMP needs its prerequisites in place. See is your instance configured for this? for the checklist: a configured backup destination, a base backup taken to it, and WAL history reaching back to your target timestamp.

Next​

  • Backups: set up the backup location that FROM ... AT TIMESTAMP depends on.
  • Point-in-time recovery: the same backup-plus-WAL machinery, used to rebuild a whole fresh instance instead of forking a database.