# Persistence Format
The `.pggraph` file is the launch artifact for fast backend startup. Format v6
stores graph-sized topology, filters, dictionaries, relationship identities,
and tenant metadata in explicitly described mmap-friendly sections. The file
is derived state; PostgreSQL source tables remain authoritative.
## File Layout
```text
Header, 512 bytes, explicitly encoded little endian
magic 4 bytes "PGGH"
version u32 current version = 6
header_size u32 512
flags u32 weights and unidirectional-edge presence
node_count u32
forward_edge_count u32
inbound_edge_count u32
section_count u32 26
body_length u64
body_crc32 u32
header_crc32 u32
reserved 16 bytes zero
section_descriptors 26 * { offset: u64, length: u64 }
reserved 32 bytes zero
Section 0 active bits ceil(node_count / 8) bytes
Section 1 table OIDs node_count * 4
Section 2 primary-key offsets (node_count + 1) * 8
Section 3 primary-key UTF-8 bytes
Sections 4-9 forward CSR offsets, targets, type IDs,
schema direction, optional
weights, relationship IDs
Sections 10-15 inbound CSR same parallel arrays
Section 16 resolution index
Section 17 filter catalog column descriptors and names
Section 18 filter values canonical typed arrays
Section 19 filter text dictionaries lexical offsets and UTF-8 bytes
Section 20 edge-type registry
Section 21 relationship identity descriptors mapping ID and key range
Section 22 relationship identity key bytes UTF-8
Section 23 tenanted table OIDs sorted unique u32 values
Section 24 tenant dictionary lexical offsets and UTF-8 bytes
Section 25 per-node tenant tokens node_count * 4
```
Every section begins at a 64-byte boundary and has an explicit offset and
length, including empty sections. Both CSR directions carry a relationship-ID
sidecar. ID `0` is reserved for adjacencies without a source-row identity.
## Write And Publication Path
Persisted build and vacuum do not construct a complete owned `Engine`. The
production path:
1. Reads the fenced PostgreSQL snapshot in bounded SPI batches.
2. Stages nodes, resolution, filters, dictionaries, tenants, both relationship
orientations, and relationship identities in checksummed external-sort runs.
3. Merges runs with a fixed fanout under memory, disk, file-count, row, work,
elapsed-time, and record-size limits.
4. Computes all 26 section lengths with checked arithmetic.
5. Reserves the complete candidate size, creates a generation-specific path
with `create_new`, zero-fills it, and fsyncs the allocation.
6. Streams sequential sections and writes dense filter slots at checked final
offsets without retaining graph-sized section buffers.
7. Recomputes the body CRC with a fixed 64 KiB buffer, backpatches the header,
fsyncs the artifact, and atomically writes its sync and projection-mode
sidecars.
8. Prepares an immutable generation manifest, loads the unpublished candidate
through the production validator, and repeats the catalog, privilege, schema,
and source-watermark checks.
9. Publishes the generation manifest and switches `projection-current.json`
only if the generation observed before the build is still current.
The previous generation remains available for rollback and pinned readers.
Scan, run, merge, quota, write, checksum, fsync, load, and source-validation
failures remove only private staging state. After publication begins, cleanup
first verifies the current pointer: a candidate that is current, or whose state
cannot be read conclusively, is preserved so an ambiguous post-rename fsync
error cannot invalidate the serving generation. The nonpersisted build path
keeps the owned builder for backend-local use and differential testing.
## Load Path
`load_graph_file()`:
```text
open file
copy into anonymous mapping
make snapshot read-only
validate header size
validate magic
validate version
read counts and offsets
validate CRC
validate section layout
validate CSR and primary-key offset content
construct snapshot-backed NodeStore arrays
construct snapshot-backed forward and inbound EdgeStore CSR arrays
keep both CSR relationship-ID arrays snapshot-backed
keep ResolutionIndex section snapshot-backed
construct FilterIndex over mapped typed values and lexical dictionaries
construct relationship identity store over mapped descriptors and key bytes
construct tenant index over mapped lexical dictionary and dense node tokens
decode only bounded column and edge-label metadata into backend heap
set resolution mode to MmapBacked
store snapshot Arc in Engine._mmap
read sync checkpoint
return Engine
```
The node arrays, both CSR directions, both relationship-ID sidecars, resolution
index, filter values and dictionaries, and relationship identity data are views
over one backend-local immutable mapping after load. Mutable filter changes use
sparse backend-local deltas. Relationship identities added after load use an
owned suffix overlay without copying the mapped base.
## Validation Boundaries
Loader validation rejects:
| Check | Failure reason |
|---|---|
| File smaller than header | Cannot read fixed header |
| Magic mismatch | Not a graph file |
| Version mismatch | Incompatible format |
| CRC mismatch | Corrupt payload |
| Section offsets out of order or out of file | Invalid layout |
| Required alignment missing | Unsafe typed slice would be invalid |
| Required section too small | Out-of-bounds typed reads |
| Weights section wrong size | Parallel arrays would diverge |
| `schema_reversed` value outside `0..=1` | Edge provenance flag invalid |
| Resolution section malformed | Lookup index invalid |
| CSR offsets not monotonic | Neighbor slices invalid |
| Final CSR offset not equal to edge count | Missing or extra edges |
| Target node index out of range | Traversal could index invalid node |
| Primary-key offsets not monotonic or out of bytes | Invalid UTF-8 slice bounds |
| First/final primary-key offsets do not span the byte section | Missing or unaddressable key bytes |
| A primary-key offset pair does not select valid UTF-8 | Invalid persisted node identity |
| Edge registry index 0 not empty | Root edge type invariant broken |
| Filter catalog, typed value width, or row count mismatch | Filter reads could cross a column boundary |
| Text dictionary offsets are invalid or values are not lexical | Token lookup would be ambiguous |
| Relationship descriptor or UTF-8 key range is invalid | Source-row identity would be ambiguous |
| Tenant OIDs, dictionary offsets/order, or node tokens are invalid | Tenant visibility would be ambiguous |
The section-layout validator and the crate-private mapped-array constructors
both enforce the value invariants used by typed access over the immutable
snapshot. This deliberate second check keeps malformed fixtures and loader call sites
from constructing a mapped store even if they bypass file parsing.
## Mmap Lifetime
The loader copies the file into an anonymous mapping and makes it read-only
before typed views are created. The private `MappedGraphArtifact` owns that
`Arc` and constructs node and edge stores only after the complete layout
validates. Each mapped store retains an `Arc` clone; `Engine._mmap` retains
another clone for resolution lookup and memory accounting. Safe store access
therefore cannot outlive its mapping, and source-inode writes or truncation
cannot change the snapshot.
```text
Engine
_mmap shares ownership for resolution and accounting
node_store owns validated ranges plus Arc
forward and inbound edge stores own validated ranges plus Arc
resolution mode references _mmap section
filter_index owns mapped base ranges plus sparse heap deltas
relationship identities own mapped base ranges plus an optional heap suffix
tenant index owns validated mapped dictionary and token ranges
edge_type_registry owns bounded heap strings decoded from its section
```
## Compatibility Rules
Version 6 extends the explicit v5 layout with persisted tenant-table metadata,
a lexical tenant dictionary, dense per-node tenant tokens, and a durable
unidirectional-edge flag. Version 5 and earlier artifacts are derived state and
are not read by this loader; users should run `SELECT graph.build()` to
regenerate the current artifact after upgrading. Future versions also fail
closed with the same rebuild guidance.
Any future change to fixed section ordering, binary representation, header
semantics, or persisted value interpretation should bump the format version and
provide a clear regeneration path. The current loader rejects non-current
versions with an incompatible-version error instructing users to rebuild.
## Fuzz Surface
The fuzz target `fuzz/fuzz_targets/load_graph_file.rs` exercises the loader.
Keep parser and loader checks total and non-panicking for arbitrary bytes.