This is the AS-BUILT storage contract owned by codegraph-store.
- Current schema version: 8
- Base DDL:
crates/codegraph-store/src/schema.rs - Ordered migrations:
crates/codegraph-store/src/migrations.rs - Connection and lease policy:
crates/codegraph-store/src/connection.rs - Canonical schema oracle:
reference/golden/*/schema.sqlandcrates/codegraph-store/tests/schema_parity.rs
scripts/docs-check.py derives the version from source and checks this document,
so version prose cannot silently drift.
A new database is configured before its first page is written, then receives the
complete base schema. The base DDL inserts schema row 1; initialization also
records the current version with description Initial schema includes all migrations. Existing databases run every missing migration in ascending order,
each in its own transaction.
Migrations that add a column first inspect PRAGMA table_info. This makes replay
safe when a newer base schema already contains the column while still failing
loudly if the table itself is absent.
| Version | Change |
|---|---|
| 1 | Core nodes, edges, files, unresolved references, FTS5, triggers, and indexes |
| 2 | project_metadata; unresolved-ref file/language context; edge provenance |
| 3 | idx_nodes_lower_name expression index |
| 4 | Drop redundant single-column edge source/target indexes |
| 5 | nodes.return_type for receiver-type inference |
| 6 | unresolved_refs.reference_subkind for structural/framework evidence |
| 7 | Deduplicate edges and add their unique semantic identity index |
| 8 | files.generated plus the partial generated-file index |
Schema version and extraction version are independent. A DDL change needs a schema migration; a change in graph meaning may need an extraction-version move even when DDL is unchanged.
| Column | Type | Contract |
|---|---|---|
version |
INTEGER primary key | applied schema version |
applied_at |
INTEGER not null | epoch milliseconds |
description |
TEXT nullable | migration description |
| Column | Type | Contract |
|---|---|---|
id |
TEXT primary key | stable node ID |
kind |
TEXT not null | serialized NodeKind |
name |
TEXT not null | display/local name |
qualified_name |
TEXT not null | language/container-qualified name |
file_path |
TEXT not null | project-relative / path |
language |
TEXT not null | serialized Language |
start_line, end_line |
INTEGER not null | 1-based source lines |
start_column, end_column |
INTEGER not null | extractor columns |
docstring |
TEXT nullable | documentation text |
signature |
TEXT nullable | callable/declaration signature |
visibility |
TEXT nullable | language visibility |
is_exported, is_async, is_static, is_abstract |
INTEGER default 0 | boolean flags |
decorators, type_parameters |
TEXT nullable | JSON arrays |
return_type |
TEXT nullable | normalized result type (migration 5) |
updated_at |
INTEGER not null | epoch milliseconds; canonicalizer removes it |
The ordinary symbol ID formula is documented in
equivalence.md. Framework-created nodes with an explicitly
documented compatibility ID are the exception, not a second general formula.
| Column | Type | Contract |
|---|---|---|
id |
INTEGER primary key autoincrement | row identity; not semantic |
source, target |
TEXT not null | foreign keys to nodes(id), cascade on delete |
kind |
TEXT not null | serialized EdgeKind |
metadata |
TEXT nullable | JSON object |
line, col |
INTEGER nullable | source location when known |
provenance |
TEXT nullable default NULL | resolution/synthesis provenance |
An edge's semantic identity is:
(source, target, kind, IFNULL(line, -1), IFNULL(col, -1))
idx_edges_identity is unique on that tuple. INSERT OR IGNORE therefore
removes repeat-pass duplicates, including coordinate-less edges.
| Column | Type | Contract |
|---|---|---|
path |
TEXT primary key | project-relative path |
content_hash |
TEXT not null | full SHA-256 content digest |
language |
TEXT not null | serialized language ID |
size |
INTEGER not null | bytes |
modified_at, indexed_at |
INTEGER not null | epoch milliseconds |
node_count |
INTEGER default 0 | extracted nodes for the file |
errors |
TEXT nullable | JSON array of extraction errors |
generated |
INTEGER not null default 0 | path/header generated-file signal (migration 8) |
Migration 8 deliberately does not read source to backfill generated; older rows
remain zero until re-extraction. Readers combine the stored flag with compatible
path classification where required.
| Column | Type | Contract |
|---|---|---|
id |
INTEGER primary key autoincrement | row identity |
from_node_id |
TEXT not null | foreign key to source node, cascade on delete |
reference_name, reference_kind |
TEXT not null | unresolved target evidence |
line, col |
INTEGER not null | reference location |
candidates |
TEXT nullable | JSON array |
file_path |
TEXT not null default '' |
source file context (migration 2) |
language |
TEXT not null default unknown |
source language (migration 2) |
reference_subkind |
TEXT nullable | structural/framework label (migration 6) |
| Column | Type | Contract |
|---|---|---|
key |
TEXT primary key | metadata name |
value |
TEXT not null | serialized value |
updated_at |
INTEGER not null | epoch milliseconds |
nodes_fts is an external-content FTS5 table over nodes:
CREATE VIRTUAL TABLE nodes_fts USING fts5(
id,
name,
qualified_name,
docstring,
signature,
content='nodes',
content_rowid='rowid'
);nodes_ai, nodes_ad, and nodes_au maintain FTS rows after node insert,
delete, and update. No tokenizer is specified, so the bundled SQLite FTS5 default
applies. The bundled build must support FTS5; schema creation fails instead of
silently switching to a weaker search backend.
FTS5 shadow tables and SQLite-owned sqlite_sequence/sqlite_stat1 can appear in
.schema. The canonicalizer accounts for the expected internal objects while
preserving application DDL.
The base schema creates:
- node indexes on kind, name, qualified name, file path, language,
(file_path, start_line), andlower(name); - edge indexes on kind,
(source, kind),(target, kind), provenance, and the unique semantic identity tuple; - file indexes on language, modified time, and the partial
generated = 1set; - unresolved-reference indexes on source node, name, file path, and
(from_node_id, reference_name).
The former idx_edges_source and idx_edges_target are intentionally absent:
the composite indexes cover their left-prefix lookups and migration 4 removes
them from old databases.
Normal write-capable connections are configured in this order:
busy_timeout = 5000 ms
foreign_keys = ON
journal_mode = WAL
journal_size_limit = CODEGRAPH_WAL_VALVE_MB (default 256 MiB)
synchronous = NORMAL
cache_size = -64000
temp_store = MEMORY
mmap_size = 268435456A rebuild validates its exclusive IndexLease between file-touching PRAGMAs.
Bulk indexing temporarily uses synchronous=OFF, a larger cache/mmap window, and
normally disables WAL autocheckpoint; a fallible finalizer restores defaults,
compacts, and publishes the completed namespace. Early failure leaves the
building namespace unreadable rather than publishing a partial database.
Readers and writers are state-gated. The permanent lock file is a capability tied to the resolved database parent; a path string or stale PID alone is not write authority.
Run the store tests and full oracle rather than comparing prose:
cargo test --locked -p codegraph-store schema_parity
cargo test --locked -p codegraph-bench --test equivalenceThe oracle normalizes schema syntax consistently, but it does not make SQLite
file bytes reproducible. Database page counters, WAL state, and row IDs are not
canonical graph output; committed schema.sql and canonical JSON are.