Skip to content

Graft Objects

Graft repository mode is content-addressed, but it does not store ordinary file blobs. A Git blob usually stores file bytes. A Graft blob currently stores a SQLite database snapshot descriptor. The actual page data and storage logs live in volume storage; repository objects describe how to name, verify, and organize those snapshots.

.graft/
objects/ # repository objects: blob, tree, commit, tag
refs/ # branch refs
index.toml # staging area
store/ # volume storage: pages, logs, storage commits
Terminal window
mkdir /tmp/graft-objects-demo
cd /tmp/graft-objects-demo
graft init app.db
find .graft/objects -type f

Right after initialization, the object database is usually empty. Graft has created the repository structure, but no repository history has been written yet.

Every repository object has an ID derived from its canonical bytes:

ObjectId = BLAKE3(canonical object bytes)

If the object content changes, the ID changes. If the content is identical, the ID is identical.

After changing a database and staging it:

Terminal window
graft sql "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"
graft sql "INSERT INTO users (name) VALUES ('Ada')"
graft add app.db

Graft writes a sqlite-snapshot-v1 blob. Its payload looks like this:

sqlite-snapshot-v1
volume <VolumeId>
page_count <PageCount>
range <LogId> <start_lsn> <end_lsn>
commit <lsn> <storage_commit_hash>
commit <lsn> <storage_commit_hash>

The blob does not contain a full copy of app.db. It records how to find and verify the database state inside volume storage:

  • volume identifies the logical database volume.
  • page_count records the snapshot’s page count.
  • range identifies the storage log intervals needed for reconstruction.
  • commit pins each LSN to the expected storage commit hash.

The outer repository object bytes are:

graft-object 1 blob <payload-bytes>\0
sqlite-snapshot-v1
volume <VolumeId>
page_count <PageCount>
...

The object ID is the BLAKE3 hash of those canonical bytes.

A blob answers “what snapshot is this?” It does not answer “where does this snapshot live in the project?”

That is the job of a tree object:

tree-v1
160000 <blob-object-id> app.db
160000 <blob-object-id> analytics/events.db

Each entry contains a mode, an object ID, and a path. In Graft, mode 160000 means this entry is a SQLite database snapshot entry.

A tree describes one complete project state, but it does not record who created that state, what history it follows, or what message explains it. A commit object adds that information:

tree <tree-object-id>
parent <parent-commit-object-id>
author-name Graft
author-email [email protected]
author-time <timestamp-ms>
author-tz +0000
committer-name Graft
committer-email [email protected]
committer-time <timestamp-ms>
committer-tz +0000
graft-version <repository-format-version>
create users table

When you run:

Terminal window
graft commit -m "create users table"

Graft reads staged database states, writes a tree, writes a commit that points at that tree and its parents, then moves the current branch ref to the new commit ID.

refs/heads/main
|
v
commit
|
v
tree
|
v
sqlite-snapshot blob
|
v
volume storage logs and pages

Loose objects use a fan-out path layout. If an object ID starts with:

9df6d4c4f0f8...

Graft stores it at:

.graft/objects/9d/f6d4c4f0f8...

Unlike Git loose objects, current Graft loose objects are written as canonical bytes rather than zlib-compressed content:

graft-object <version> <kind> <payload-bytes>\0<payload>

When Graft reads an object, it decodes those bytes and recomputes the object ID. If the path ID and computed ID differ, the object is invalid.

The index is the draft for the next tree. graft add app.db writes a snapshot blob and records a staged entry in .graft/index.toml. graft commit turns staged entries into a tree and clears the index.

The object database and volume storage have different responsibilities:

  • .graft/objects stores repository history: blobs, trees, commits, and tags.
  • .graft/store stores physical database data: pages, logs, storage commits, and volume indexes.

A commit is therefore not a full database file. It is a verifiable route to one: commit -> tree -> sqlite snapshot blob -> volume storage.

To materialize a revision as a normal SQLite file:

Terminal window
graft export --source HEAD --output app.head.db app.db
sqlite3 app.head.db "SELECT * FROM users;"

The exported file is an ordinary SQLite database snapshot. It is no longer the Graft volume itself.