3. Storage & Persistence Layer
Batho's storage subsystem provides a pure Apache Arrow IPC-based persistence layer that replaces all SQLite dependencies. It is organized into three components: the Arrow Bundle (transport artifact and working copy), the Arrow Store (BSG graph scratch space), and the Unified Cache (cross-session AST and file tracking).
3.1 Arrow Bundleβ
The Arrow Bundle is the primary durable artifact format. It stores all code intelligence data β entities, relationships, BSG views, file tracking, run metadata, and changelogs β as memory-mappable Arrow IPC files under .batho/artifact/.
Architectureβ
Figure 25: Arrow Bundle Architecture β Component view showing the Bundle faΓ§ade delegating to writer, MVCC manager, reader, and change detection engine.
Bundle FaΓ§adeβ
The Arrow Bundle exposes a unified public API that replaces the legacy database interface. All Batho commands β build, patch, export, gc, diff, and fix β interact exclusively through this faΓ§ade. A single shared instance is maintained per repository, ensuring consistent state across all operations.
MVCC Generation Lifecycleβ
The Bundle Manager implements a multi-version concurrency control (MVCC) pattern for atomic writes:
Figure 26: MVCC Generation Lifecycle β Atomic commit process ensuring zero-copy readers never observe partial writes.
Key guarantees:
- Writers commit new Arrow IPC generations atomically by writing to
.tmpfiles, renaming to.v<N>.ipc, then swappingmeta.json. - Active readers continue to hold their memory map on the old generation.
- Old generations are cleaned by
batho gc. - Transport ZIP artifacts (
.bathofiles) are produced bybatho export --pack.
Zero-Copy Readerβ
The Arrow Bundle reader provides zero-copy, memory-mapped reads with O(1) point lookup:
- On first access to a logical table, reads the active generation path from
meta.json. - Opens it via memory-mapped I/O (zero-copy).
- Builds an offset index mapping file IDs to table slices.
Subsequent lookups use the index for O(1) slice operations, avoiding full table scans.
Incremental Writerβ
The writer accumulates rows into in-memory column buffers and flushes them as unified, uncompressed IPC files sorted by file ID. A flush threshold of 50,000 rows prevents excessive memory usage during large builds. Remaining rows are flushed before the manager commits the generation.
Table Schemasβ
The Arrow Bundle defines seven logical tables under schema version batho-bundle.v1:
| Table | Purpose | Key Columns |
|---|---|---|
runs | Index run metadata | run_uuid, status, git_commit, entity_count, rel_count |
string_dict | Global string deduplication | id (int64), val (large_utf8) |
file_tracking | File β hash/mtime/inode/size mapping | file_id, file_path, content_hash, mtime_ns, is_indexed |
agent_views | BSG agent view entities (compressed) | file_id, entity_id, name, entity_type, signature, fqn |
storage_views | BSG storage view entities (full fidelity) | file_id, entity_id, raw_content, raw_bytes, start_byte, end_byte |
rels_views | BSG relationship view | file_id, source_id, target_id, relation_type, metadata_json |
file_changelog | Flattened NodeDiff rows for incremental patches | run_uuid, file_id, entity_id, change_kind, changed_fields |
run_artifacts | Telemetry/metrics/audit JSON per run | run_uuid, context_overview_json, telemetry_json, security_audit_json |
Key Minificationβ
Entity and relationship dictionaries use compact key mapping to reduce serialized payload sizes by 30β40%. For example, entity_type is stored as ty, name as n, and start_line as sl. The syntax_glue object is similarly minified.
Incremental Change Detectionβ
The change detection engine performs native hash-based scanning against the file tracking table. It replaces the legacy Git-based change detection and compares filesystem modification times and SHA-256 hashes:
- Unchanged files: Skipped immediately.
- Added/Modified files: Parsed and merged into the hypergraph.
- Deleted files: Removed from the active index.
3.2 Arrow Storeβ
The Arrow Store is a persistent Arrow IPC scratch store that replaces the four legacy SQLite scratch tables (entity_dict, query_entities, query_relationships, dangling_references).
Directory Layoutβ
.batho/bsg/
βββ current/ β shared store (build + patch update in-place)
β βββ entity_dict.ipc # integer key β entity ID string
β βββ entities.ipc # query_entities equivalent (columnar)
β βββ relationships.ipc # query_relationships equivalent (columnar)
β βββ dangling.ipc # dangling_references equivalent (columnar)
β βββ meta.json
β βββ _stream/ # staging during bulk-insert (transient)
β βββ entities_stream.ipc.zst
β βββ relationships_stream.ipc.zst
β βββ dangling_stream.ipc.zst
β
βββ <patch_uuid>/ β per-patch delta sidecar (changed-file rows only)
βββ entities.ipc
βββ relationships.ipc
βββ meta.json
Two-Phase Compactionβ
Figure 27: Arrow Store Compaction Pipeline β Two-phase design separating append-friendly streaming writes from final memory-mapped compacted files.
Why IPC File format for at-rest files:
- Supports random access and memory-mapping (zero-copy reads).
- No decompression overhead on every read.
- OS pages in only touched columns/rows.
The _stream/ staging files use IPC Stream + zstd during bulk-insert because they are append-friendly and transient (deleted after compaction).
Scratch Store Tablesβ
Schema version: bsg-arrow-store.v1
| Table | Purpose | Key Columns |
|---|---|---|
entity_dict | Integer key β opaque entity ID string | id (int64), val (large_utf8) |
entities | Columnar entity store | entity_key, entity_name (dictionary), entity_type (dictionary), fqn, file_path (dictionary), line_number, signature, is_exported |
relationships | Columnar relationship store | source_key, target_key, relation_type (dictionary), metadata_json |
dangling | Dangling/unresolved references | source_key, unresolved_target_name (dictionary), relation_type (dictionary) |
Dictionary-encoded columns (entity_name, entity_type, file_path, relation_type) reduce memory footprint by 60β80% compared to plain string storage.
In-Process Metricsβ
Run metrics are computed in-process using Arrow column operations, replacing 8 SQL queries. The metrics engine reads compacted IPC files and the bundle's file artifacts table to produce context overview, structural metrics, and artifact payload dictionaries.
3.3 Unified Cacheβ
The Unified Cache service provides disk-persistent AST caching, file tracking delegation, and in-memory file snapshots.
Cache Architectureβ
Figure 28: Unified Cache Architecture β Delegation layers showing how the Unified Cache routes AST results to msgpack flat-files, file tracking to the Arrow Bundle, and snapshots to in-memory storage.
Cache Variant Systemβ
AST cache entries are tagged with a variant key derived from parsing configuration. This ensures that cache entries produced with different parsing configurations (e.g., bidirectional mode with gap entities vs. standard mode) do not collide. The variant key is a short hash of the schema version, gap inclusion flag, and parsing parameters.
Pattern-Based Cache Invalidationβ
Cache invalidation supports three modes:
| Pattern | Behavior | Example |
|---|---|---|
* or ** | Clear entire cache | All entries removed |
| Exact path | Delete single entry | One file's cache entry removed |
| Directory prefix | Delete by path prefix | All files under a directory removed |
| Glob pattern | Pattern scan + delete | All matching files removed |
The entire read+delete sequence for glob patterns is kept inside a manifest lock to prevent TOCTOU races where freshly written entries could be deleted after the manifest snapshot but before per-file deletion.
Cache Statisticsβ
Cache statistics provide a unified view across all cache layers:
| Metric | Source |
|---|---|
ast_cache_enabled | Whether AST cache directory is configured |
snapshot_count | In-memory snapshot dict size |
file_tracking_count | Rows in bundle file_tracking table |
bundle_dir | Active artifact directory path |