# 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` 自动化验证。
