- 版本化 SQLite 迁移(向前/回滚)与连接助手。 - 导入管线:上传文件按 SHA-256 内容哈希不可变保存,文件/行/批次 幂等;重复上传返回 duplicate 并复用已有批次,不产生第二份事实。 - 每条规范化源行反查文件、工作表、原始行号与模板版本。 - 解析失败保留 exception/failed 批次与诊断,不产生已确认交易。 - 决策记录见 docs/decisions/002-persistence.md。
2.5 KiB
2.5 KiB
002 持久化层技术决策
对应 Issue:B-39(docs/issues/002-p0-persistence-and-immutable-imports.md)。
数据库:SQLite(Python 标准库 sqlite3)
- 单文件、事务完整、零新增依赖,与当前标准库服务器和离线内网部署环境匹配。
- 外键约束、CHECK 约束、唯一约束和触发器均可用,足以承载不可变证据模型。
- 金额以
TEXT保存Decimal的原始字符串,读取时还原为Decimal, 绝不经过二进制浮点。 - 后续若并发写入成为瓶颈,可开启 WAL 或平迁 PostgreSQL;迁移版本表
schema_migrations不绑定具体引擎方言之外的特性。
迁移工具:仓库内置版本化迁移器(src/bank_importer/db.py)
- 每条迁移包含
up/down两段 SQL,按版本号顺序执行并记录在schema_migrations表中;重复执行无副作用。 - 不引入 Alembic 等外部工具:当前模型规模小,内置迁移器保持零依赖,
且回滚路径明确(
python -m bank_importer.db <db路径> --rollback-to <版本>)。 - 服务启动时自动执行
migrate,空数据库即可完整建表。
文件存储:内容寻址的本地文件系统(data/files/)
- 上传文件先计算 SHA-256,再写入
data/files/<哈希前两位>/<完整哈希>.<扩展名>。 - 发布采用临时文件 + 硬链接:目标要么完整出现、要么不存在,且永不覆盖。
- 相同内容(即使文件名不同)只保存一份文件、一条
source_files记录。 data/已加入.gitignore,真实流水不进入版本库。
幂等与状态机
- 文件级幂等:
source_files.sha256唯一约束。重复上传插入一条status='duplicate'的批次审计记录,指向首个批次,不产生第二份事实。 - 行级幂等:
source_rows上UNIQUE (sheet_batch_id, source_row)。 - 批次状态:
parsing → parsed | exception | failed,另有duplicate。 失败/异常批次保留import_exceptions诊断(阶段、消息、原始文件名), 不产生任何已确认源行。 - 事务边界:文件落盘后,
source_files+ 批次 + 工作表批次 + 源行在单个 SQLite 事务内提交;解析失败时批次状态与异常记录同样在事务内落库。
不可变性
source_files、sheet_batches、source_rows三张表由数据库触发器禁止UPDATE和DELETE,任何修改只能走后续阶段的冲销/审计调整流程。- 该约束由
tests/test_persistence.py自动化验证。