Branching
PyroSQL branching works like Git branches, but for data. A branch is a lightweight, copy-on-write snapshot of the entire database. It can be queried and mutated independently of its parent, then either merged back or discarded — without affecting any other branch.
Because branches use copy-on-write storage, creating a branch is nearly instantaneous and consumes no additional disk space until rows are actually modified on the branch.
Use cases
- Feature branches — develop schema migrations or data transformations in isolation before promoting them to production.
- Staging environments — give each developer or CI job its own live copy of production data without duplicating storage.
- A/B testing — run two variants of an application against diverging data sets and merge the winning variant.
- Safe bulk operations — run a destructive
UPDATEorDELETEon a branch, verify the result, then merge; discard on failure.
PyroBranchManager
PyroBranchManager is the entry point for all branch lifecycle operations. It requires a DBAL connection and a PyroSqlDriver instance, and all its methods assert that the connection is backed by PyroSQL before executing.
use Weaver\ORM\PyroSQL\Branch\PyroBranchManager;
use Weaver\ORM\PyroSQL\PyroSqlDriver;
$driver = new PyroSqlDriver($connection);
$manager = new PyroBranchManager($connection, $driver);
create(string $name, string $from = 'main', ?DateTimeImmutable $asOf = null): PyroBranch
Create a new branch from an existing one. By default branches off main at the current state. Pass $asOf to branch from a historical snapshot.
// Branch off the current state of main
$branch = $manager->create('feature/new-pricing');
// Branch off main as it existed on a specific date
$branch = $manager->create(
name: 'audit/q1-snapshot',
from: 'main',
asOf: new \DateTimeImmutable('2024-03-31 23:59:59'),
);
Executes:
CREATE BRANCH "feature/new-pricing" FROM "main"
-- or
CREATE BRANCH "audit/q1-snapshot" FROM "main" AS OF TIMESTAMP '2024-03-31 23:59:59'
list(): PyroBranch[]
Return all branches visible on the current connection, ordered by creation time ascending.
foreach ($manager->list() as $branch) {
printf(
"%-30s parent: %-20s created: %s\n",
$branch->getName(),
$branch->getParentName(),
$branch->getCreatedAt()->format('Y-m-d H:i:s'),
);
}
exists(string $name): bool
Check whether a branch with the given name exists.
if (!$manager->exists('feature/new-pricing')) {
$branch = $manager->create('feature/new-pricing');
}
delete(string $name): void
Permanently drop a branch. Throws BranchNotFoundException if the branch does not exist.
$manager->delete('feature/old-experiment');
switch(string $name): void
Switch the session context to the named branch. All subsequent queries on the current connection are routed to this branch until switched again.
$manager->switch('feature/new-pricing');
// All queries now read from / write to the feature/new-pricing branch
$products = $productRepo->query()->where('active', true)->get();
Executes:
SET pyrosql.branch = 'feature/new-pricing'
PyroBranch
PyroBranch is a value object representing a branch. Obtain instances via PyroBranchManager::create() or PyroBranchManager::get().
connection(): Doctrine\DBAL\Connection
Set the session context to this branch and return the DBAL connection. All subsequent queries on the returned connection are scoped to the branch.
$branch = $manager->create('feature/new-pricing');
$conn = $branch->connection();
// Use $conn directly for raw queries on this branch
$conn->executeStatement("UPDATE products SET price = price * 1.05");
mergeTo(string $targetBranch = 'main'): void
Merge this branch back into another branch (defaults to main). Changes made on the branch are applied to the target.
$branch->mergeTo('main');
// or merge into a different branch
$branch->mergeTo('staging');
Executes:
MERGE BRANCH "feature/new-pricing" INTO "main"
delete(): void
Drop this branch permanently.
$branch->delete();
storageBytes(): int
Returns the number of bytes consumed by this branch's copy-on-write delta versus its parent. A freshly created branch with no modifications returns 0.
$bytes = $branch->storageBytes();
printf("Branch %s uses %.2f MB of storage.\n", $branch->getName(), $bytes / 1_048_576);
Branch naming
Branch names follow the same rules as PyroSQL identifiers. Names may contain letters, digits, hyphens, underscores, and forward slashes (e.g. feature/my-branch). Quoting is handled automatically by PyroBranchManager.
Full example: create a branch, modify data, merge back
use Weaver\ORM\PyroSQL\Branch\PyroBranchManager;
use Weaver\ORM\PyroSQL\PyroSqlDriver;
$driver = new PyroSqlDriver($connection);
$manager = new PyroBranchManager($connection, $driver);
// 1. Create a feature branch off main
$branch = $manager->create('feature/price-increase');
// 2. Switch the session to the branch
$branchConn = $branch->connection();
// 3. Make changes on the branch
$branchConn->executeStatement(
"UPDATE products SET price = ROUND(price * 1.10, 2) WHERE category = 'electronics'"
);
// 4. Verify the changes look correct
$branchConn->executeStatement("SET pyrosql.branch = 'feature/price-increase'");
$affectedCount = (int) $branchConn->fetchOne(
"SELECT COUNT(*) FROM products WHERE category = 'electronics'"
);
printf("Updated %d products on branch.\n", $affectedCount);
// 5. Check storage overhead
printf("Branch storage: %d bytes\n", $branch->storageBytes());
// 6a. Merge the branch into main if satisfied
$branch->mergeTo('main');
$branch->delete();
// 6b. Or just discard if not satisfied
// $branch->delete();