Skip to content

SQLite Snapshots

Graft tracks SQLite database paths as snapshots. A snapshot is an immutable view of a database state that can be recorded in a repository commit.

That makes a SQLite database path a first-class versioned object in Graft. The repository does not have to treat data.sqlite as an opaque binary blob: it can record a snapshot, compare supported row and schema changes, merge compatible edits, and materialize the result back to a normal SQLite file.

Normal applications still see SQLite files:

project/
data.sqlite
.graft/

data.sqlite is the worktree path. The commit stores a snapshot descriptor backed by Graft storage under .graft/.

The default mode uses an ordinary SQLite file:

Terminal window
graft sql --db data.sqlite "INSERT INTO notes(body) VALUES ('hello');"

The SQL transaction changes data.sqlite and possibly its WAL. It does not write Graft storage directly. Staging is the storage boundary:

Terminal window
graft add data.sqlite
graft commit -m "add note"

graft add uses SQLite’s online backup API to capture one committed database state. It includes committed WAL frames without checkpointing or modifying the source database, normalizes the private snapshot into a standalone rollback- journal file, and compares its image with the staged or committed base in 4 KiB Graft storage chunks. Only changed chunks are appended to Graft storage. The physical database can use a larger legal SQLite page size, such as 8 KiB; storage deduplication remains 4 KiB. If nothing changed, the existing CommitFileState and snapshot are reused.

Rollback-journal databases follow the same path; there simply may be no WAL to include. Uncommitted transactions are never staged.

By default, Graft materializes committed SQLite snapshots back to normal database files in the worktree after commit, checkout, switch, pull, and merge-continue.

That lets standard SQLite tools inspect the current state:

Terminal window
sqlite3 data.sqlite "SELECT count(*) FROM notes;"

The repository commit still stores a verifiable snapshot descriptor, not a plain copy of the whole file.

Use graft export when you want a separate normal SQLite file:

Terminal window
graft export --source HEAD --output data.head.sqlite data.sqlite

Export does not move HEAD, change the index, or connect the exported file to the current Graft volume.

If a standard SQLite tool edits a tracked materialized database directly, commit the transaction and stage the path:

Terminal window
sqlite3 data.sqlite "INSERT INTO notes(body) VALUES ('external edit');"
graft add data.sqlite

Manual WAL checkpointing is not required. Before checkout, switch, restore, or another operation that replaces the worktree file, close long-lived database connections. Graft obtains an exclusive SQLite lock, folds a WAL into the old main file, removes sidecars, and refuses replacement while another writer is active.

The SQLite extension can open a path with vfs=graft when an application wants SQLite commits to advance a live Graft Volume directly. This is an advanced data-plane mode, not the repository control plane. Repository status, staging, history, merge, and sync still use the CLI. Unlike physical staging, the live Graft VFS write path requires SQLite page_size=4096 because each SQLite page maps directly to one Graft page.

Do not edit the same logical database simultaneously through the Graft VFS and the platform’s normal file VFS. Choose one write path and close live connections before branch operations that replace or rebind database state.