colbymchenry/codegraph · src/db/schema.sql · src/sync/

一个文件就是整张代码图

CodeGraph 不用图数据库,而是把整张代码知识图谱塞进一个本地 SQLite 文件——靠 Node 内置的 node:sqlite + FTS5 全文索引,靠 files 表的内容哈希做增量同步。本页基于真实的 schema.sql(Version 1)与 src/sync/ 拆解:表怎么设计、FTS 怎么保持同步、检索怎么三级回退、增量 sync 怎么只碰变了的文件。

5
核心表
FTS5
外部内容 + 3 触发器
3 级
prefix→LIKE→fuzzy
hash
内容哈希增量

四张核心表 + 元数据表

图的全部状态落在五张表里:nodes(符号)、edges(关系)、files(被追踪的源文件)、unresolved_refs(待解析引用),外加一张 project_metadata 记版本/来源;另有 schema_versions 跟踪迁移版本。nodes 的列直接反映了抽取层的产物:

src/db/schema.sql — nodes(节选)
CREATE TABLE nodes (
  id TEXT PRIMARY KEY,            -- 稳定符号 ID
  kind TEXT NOT NULL,             -- function / class / route …(22 种)
  name TEXT, qualified_name TEXT,
  file_path TEXT, language TEXT,
  start_line INTEGER, end_line INTEGER, start_column INTEGER, end_column INTEGER,
  docstring TEXT, signature TEXT, visibility TEXT,
  is_exported INTEGER, is_async INTEGER, is_static INTEGER, is_abstract INTEGER,
  decorators TEXT,        -- JSON 数组
  type_parameters TEXT,   -- JSON 数组
  return_type TEXT,       -- 归一化返回类型,供接收者类型推断
  updated_at INTEGER NOT NULL
);

edges 用自增主键,带 source/target/kind/metadata(JSON)/line/provenance,并对 source、target 都加了 ON DELETE CASCADE 外键——删一个节点,连它的边自动清掉,这让增量重建一个文件时不留悬挂边。files 表是增量同步的账本:

src/db/schema.sql — files / unresolved_refs
CREATE TABLE files (
  path TEXT PRIMARY KEY,
  content_hash TEXT NOT NULL,   -- ← 变更检测的核心
  language TEXT, size INTEGER,
  modified_at INTEGER, indexed_at INTEGER,
  node_count INTEGER, errors TEXT       -- JSON
);
CREATE TABLE unresolved_refs (   -- 全量索引后才能解析的引用
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  from_node_id TEXT, reference_name TEXT, reference_kind TEXT,
  line INTEGER, col INTEGER, candidates TEXT,  -- JSON 候选
  FOREIGN KEY (from_node_id) REFERENCES nodes(id) ON DELETE CASCADE
);

FTS5 外部内容表 + 三个触发器

全文检索不是另存一份文本,而是用 FTS5 的外部内容(external-content)模式——虚拟表 nodes_fts 声明 content='nodes'、content_rowid='rowid',索引的列是 name / qualified_name / docstring / signature。它不存自己的副本,只对 nodes 表的这几列建倒排索引,省一份存储。

src/db/schema.sql — FTS5 + 同步触发器
CREATE VIRTUAL TABLE nodes_fts USING fts5(
  id, name, qualified_name, docstring, signature,
  content='nodes', content_rowid='rowid'
);
-- 插入:把新行喂进 FTS
CREATE TRIGGER nodes_ai AFTER INSERT ON nodes BEGIN … END;
-- 删除:用 'delete' 特殊指令把旧行从 FTS 撤回
CREATE TRIGGER nodes_ad AFTER DELETE ON nodes BEGIN … END;
-- 更新:先 'delete' 旧行,再插新行
CREATE TRIGGER nodes_au AFTER UPDATE ON nodes BEGIN … END;

外部内容 FTS5 有个众所周知的坑:删/改时必须显式用 INSERT INTO nodes_fts(nodes_fts, …) VALUES ('delete', …) 把旧行撤回,否则倒排索引会和真实数据漂移。三个触发器 nodes_ai / nodes_ad / nodes_au 正是把这件事下沉到数据库层自动完成——应用代码只管增删改 nodes,FTS 永远一致。

索引设计的取舍:少建一个索引

schema 里最"数据库工程师"的一笔,是 故意不建 idx_edges_source 和 idx_edges_target 这两个窄索引。注释解释得很清楚:复合索引 (source, kind) 和 (target, kind) 已经能靠 SQLite 的最左前缀扫描覆盖"只按 source"或"只按 target"的查询——再单建窄索引就是纯粹给写入加负担的死重量。迁移 v4 还会在已有库上把它们 DROP 掉。

其余索引覆盖了所有热查询路径:nodes 上有 kind / name / qualified_name / file_path / language、复合 (file_path, start_line),以及一个 表达式索引 lower(name) 支持大小写不敏感查找;edges 上有 kind / (source,kind) / (target,kind) / provenance(provenance 索引正是为了一句 SQL 筛出全部 heuristic 合成边);unresolved_refs 上有 from_node_id / reference_name / file_path 及复合 (from_node_id, reference_name)。

git 感知的增量 sync:只碰变了的文件

src/sync/ 把"保持索引新鲜"拆成几块协作的机制,核心是用 files.content_hash 做变更检测,而不是每次全量重建:

  • 内容哈希 + 增量重索引:sync 时比对每个文件当前内容哈希与 files 表里存的哈希——没变就跳过,变了才重抽取;删除的文件连带其 nodes(及级联的 edges / unresolved_refs)一起清掉。
  • git sync hooks(sync/git-hooks.ts):当文件监视关闭时,可选地挂上 commit / merge / checkout 钩子,在这些时点触发一次增量 sync。
  • 文件监视 + 监视策略(watcher.ts / watch-policy.ts):用 fs.watch,但带策略——例如 WSL2 + /mnt/* 上递归 fs.watch 太慢直接关掉(issue #199),避免启动慢到撞穿 host 的握手超时。
  • worktree 感知(sync/worktree.ts):检测一个查询是不是在"借用"另一棵 git worktree 的索引,避免跨 worktree 串味。

与 resolver 的衔接:unresolved_refs 与 provenance 列

存储层和解析层是通过两个 schema 设计点咬合的:

  • unresolved_refs 表是抽取与解析之间的暂存区:抽取时遇到的、当下还无法定位定义的引用先连同 candidates(JSON) 落这张表;全量索引完成后,resolver 再回头消解它们、写成正式 edges。这把"单文件抽取"和"跨文件解析"在存储上解耦开。
  • edges.provenance 列 + idx_edges_provenance:回调合成器写出的边统一带 provenance='heuristic' 与 metadata.synthesizedBy,索引让"筛出/审计全部合成边"成为一句 WHERE provenance='heuristic'。换言之,"这条边是谁推断出来的"被持久化进了 schema,可复现、可进 git、可追责。

意义:把图数据库的需求,压进一个 SQLite 文件

"代码知识图谱"听起来像要上 Neo4j,CodeGraph 的回答是——一个 SQLite 文件足矣。节点/边/文件/待解析引用四张表 + 外键级联给了图语义,FTS5 外部内容表 + 三触发器给了零额外存储的全文检索,复合索引的精打细算压住写入开销,files.content_hash 把同步变成增量,provenance 列把"推断来源"沉淀成可查询的事实。这套设计让 CodeGraph 的索引可进 git、可复现、零外部依赖——一个 .codegraph/codegraph.db 文件,就是 agent 共享的那张"代码索引标准件"。