Skip to main content

How It Works

Graft operates on database data files at the filesystem level. It never needs to understand SQL, table structures, or database internals.

The Core Idea

Database engines store data in a structured directory on disk. For Postgres, this is PGDATA at /var/lib/postgresql/data/. Graft copies this directory faithfully, treating the entire state as a single unit that can be snapshotted, branched, and restored.

Storage Architecture

Host Filesystem

Branch data is stored on the host under ~/.graft/<project>/:
~/.graft/
  └── my-postgres/
      ├── config.json          ← State (schema v2)
      ├── graft.db             ← SQLite DAG (commits, tree_entries, refs)
      ├── objects/             ← BLAKE3 object pool (sharded by hash)
      │   ├── a1/
      │   │   └── b2c3d4e5f6…  ← Content-addressed blob
      │   └── f0/
      │       └── e1d2c3b4a5… ← Immutable, deduplicated
      └── branches/
          ├── main/            ← Materialized working branch
          └── experiment/      ← Another branch

Content-Addressable Storage

When you run graft commit, Graft:
  1. Stops the container — Postgres must be at rest for a consistent snapshot
  2. Walks the branch directory — Every file is hashed with BLAKE3
  3. Stores blobs — Each file goes into objects/<prefix2>/<hash62> keyed by its BLAKE3 digest
  4. Builds a Merkle tree — Sorted leaves, pair-hashed up to a single root
  5. Inserts into SQLite — Commit record + tree entries + ref update
The object pool is immutable and deduplicated by content hash. Identical files (unchanged across commits) are stored exactly once.

Merkle DAG

Each commit records:
  • A 7-character BLAKE3 commit hash
  • The parent commit hash (forming a chain)
  • The Merkle tree root (cryptographic fingerprint of every file)
  • A verified flag
  • A message and timestamp
Commit A (root: 0xabc)  ← initial main branch state
  └── Commit B (root: 0xdef, parent: 0xabc)  ← experiment checkout
        └── Commit C (root: 0x123, parent: 0xdef)  ← further change

Container Lifecycle

Bind Mount Mode

Graft manages the container lifecycle by recreating it with a host bind mount:
1. Stop container
2. Run filesystem operation (commit, checkout, etc.)
3. Remove old container
4. Recreate container with bind mount → branch directory
5. Start container (if it was running before)
The container is recreated with the exact same configuration — image, environment variables, port bindings, network mode, and labels.

Smart Lifecycle

Graft captures whether the container was running before any operation. If it was already stopped, the stop/start steps are skipped. This means graft commit on a stopped container is instant after the filesystem work completes.

Branch Operations

Checkout (New Branch)

1. Look up parent branch's tip commit in SQLite
2. Get tree entries (file list + blob hashes) from the DAG
3. Materialize files from the object pool into the new branch directory
4. Set the new branch's ref to the parent's tip
5. Recreate the container with bind mount pointing at the new branch

Checkout (Existing Branch)

1. Look up branch's tip commit
2. Get tree entries
3. Compute diff against current branch directory (walk + hash)
4. Only touch files that differ (added, removed, changed)
5. Prune empty directories
6. Recreate container with bind mount

Rollback

1. Look up the target commit tree
2. Materialize those tree entries into the branch directory
3. Move the branch ref back to the target commit
4. Future commits extend from this point

Integrity Verification

On-Demand Verify

1. Walk the branch directory
2. BLAKE3-hash every file
3. Build the Merkle tree
4. Compare computed root against stored root in the DAG
5. Report per-file differences (missing, extra, changed)
If verification passes on a previously unverified commit, the verified flag is automatically set.

Verify Output

✓ Integrity OK (147 files, root f0e1d2)

✗ Integrity FAILED for experiment (2 changed, 1 missing)
  stored root: a1b2c3d
  actual root: f0e1d2
With --verbose, per-file details are shown:
STATUS    FILE                    EXPECTED HASH   ACTUAL HASH
changed   base/16384/12547       a1b2c3d4e5f6…   f0e1d2c3b4a5…
missing   global/pg_control      9876543210ab…   (none)