SQLite 存储与迁移系统
SQLite 存储与迁移系统:Claude-Mem 的具体实现
学习目标
本章聚焦 Claude-Mem 如何使用 SQLite 作为核心存储,以及 34 步增量迁移系统的运作方式。
项目实践
核心表结构
Claude-Mem 的 SQLite 数据库包含以下核心表:
迁移系统
src/services/sqlite/migrations/ 包含从 v1 到 v34 的增量迁移:
- 迁移执行器:
runner.ts检查schema_versions表,按顺序执行未应用的迁移 - 幂等性:每步迁移使用
CREATE TABLE IF NOT EXISTS和ALTER TABLE确保可重复执行 - 快照文件:
schema.sql记录迁移后的最终数据库形状,新数据库直接使用该形状 - 降级策略:不支持回滚迁移,只能向前
关键索引设计
数据库为高频查询路径创建了专门的索引:
| 表 | 索引 | 查询场景 |
|---|---|---|
observations | idx_observations_project | 按项目查询所有观察 |
observations | idx_observations_created | 按时间倒序获取最新观察 |
observations | idx_observations_content_hash | 去重检查(memory_session_id + content_hash) |
sdk_sessions | idx_sdk_sessions_project | 按项目查询会话 |
pending_messages | idx_pending_messages_status | 获取待处理消息队列 |
去重机制
observations 表的 UNIQUE(memory_session_id, content_hash) 约束替代了内存中的去重窗口:
content_hash基于观察的内容计算(非随机 ID)- 写入时使用
INSERT ... ON CONFLICT DO NOTHING天然吸收重复 - 避免 Hook 重复触发导致的重复记录
PendingMessageStore
pending_messages 表作为持久化的工作队列,替代了内存中的 pendingTools Map:
- 状态流转:
pending→processing - 唯一约束:
UNIQUE(content_session_id, tool_use_id)保留摄取配对 - 崩溃恢复:Worker 重启后可从
pending状态继续处理
问题与规避
原生模块兼容性
问题:Bun 的 bun:sqlite 模块在不同平台可能因原生依赖编译失败。
规避:ContextBuilder 中的 initializeDatabase() 捕获 ERR_DLOPEN_FAILED 错误,清理 .install-version 标记文件,提示用户重启 Claude Code 自动修复。
大表性能
问题:随着使用时间增长,observations 表可能达到数万行。
规避:
- 索引覆盖高频查询路径
- FTS5 虚拟表提供全文搜索,避免全表扫描
- 按
project和created_at_epoch的复合查询可通过组合索引优化
设计取舍
为什么用 SQLite 而非其他数据库?
| 维度 | SQLite | PostgreSQL | 文件存储 |
|---|---|---|---|
| 部署复杂度 | 零(单文件) | 需要安装和配置 | 零 |
| 查询能力 | SQL + FTS5 | 完整 SQL | 需要自建索引 |
| 并发写入 | 单写者 | 多写者 | 需要文件锁 |
| 适用场景 | 单机单用户 | 多用户服务端 | 极小规模 |
Claude-Mem 选择 SQLite 的核心原因是零部署成本:用户不需要安装额外的数据库服务。单用户场景下的并发写入问题不存在(只有一个 Worker 进程写入)。