结论与下一步
本任务要把“板上能执行 SQLite”升级为“板上事务行为有证据、模块共用一套数据库底座”。当前选择 SQLite 3.53.4 + ESP-IDF FATFS/Wear Levelling,固定 4 KiB 扇区、journal_mode=DELETE、synchronous=EXTRA、psow=0;本项目实测的 LittleFS 路线不进入生产实现。
下一步先合入共享 SQLite 内核与实板资格测试工具,再让 Schedule、TimingTask 等模块的 SQLite Adapter 复用同一个连接、迁移和事务边界。PR #101 中的 TimingTask SQLite 代码可作为领域 Adapter 骨架,但不能成为第二套数据库生命周期实现。
为什么现在必须统一
main 上的 Schedule 骨架与 PR #101 已开始分别定义持久化模型。如果每个模块自己打开数据库、设置 PRAGMA、维护迁移和解释错误,跨 Schedule、TimingTask、Outbox、幂等记录的原子提交将无法保证,后续也会出现多套恢复策略。
本任务统一的是数据库底座,不统一业务接口。领域模块继续定义有业务语义的 Store Port;不会引入 Repository<T>、字符串表名或“任意 SQL”一类通用读写协议。
公开方案调查
截至 2026-08-03,没有找到一套可以直接承担 VoiceLife 生产持久化的成熟 ESP32 SQLite 方案:
方案
调查结果
本项目决定
siara-cc/esp32-idf-sqlite3 / Arduino 版本
社区使用最多,但可见实现曾使用内存 journal、空实现 truncate/lock,Arduino VFS 还忽略 fsync 返回值;仓库已有损坏与事务相关报告
不直接依赖
jTecRepos/jSQLite3
重写文件层,但无采用度、无掉电恢复测试,最后提交停在 2023 年
只作代码参考
huming2207/sqlite_espbdl
2026-07 出现的原始块设备 VFS,方向有价值;当前 0 star、0 fork、无测试目录,并依赖 ESP-IDF 6.2 master
作为后续性能对照,不作为当前依赖
SQLite unix-none + FATFS/WL
使用 ESP-IDF 官方文件系统和磨损均衡能力;VoiceLife 已在目标 ESP32-S3 上完成外部 EN 复位恢复验证
当前实现基线
判断依据不是 star 数,而是 SQLite 对 VFS 的同步、截断、锁、扇区和删除语义有明确假设。参考:
实板证据
目标板:ESP32-S3,ESP-IDF 6.0.2,2 MiB voicelife 测试分区,串口固定 115200。
被否决的路线
SQLite unix-none 运行于 joltwallet/littlefs 1.22.3 时,以下三组配置都在显式 ROLLBACK 后留下了表记录,而索引记录为 0:
PERSIST + FULL + psow=1 -> FAIL
PERSIST + FULL + psow=0 -> FAIL
DELETE + FULL + psow=0 -> FAIL
Explicit rollback integrity: table_rows=1 index_rows=0
PROBE_RESULT: FAIL step=explicit rollback leaked row
因此,“LittleFS 自身支持掉电恢复”不能推出“SQLite 的 VFS 假设在该组合上成立”。当前测试栈明确拒绝 LittleFS + SQLite。
当前通过的路线
FATFS/WL 使用 4 KiB 扇区,SQLite 使用 DELETE + EXTRA + psow=0。主机在两个标记点通过串口控制线触发 EN 复位,设备启动原因均为 rst:0x1 (POWERON):
脏页已刷出、事务尚未提交;重启后 24 条测试记录全部回滚。
COMMIT 返回后立即复位;重启后已提交标记仍存在。
三轮独立测试全部通过。每轮还覆盖显式回滚、40 次四表原子提交、每次提交后的表/索引扫描一致性、PRAGMA quick_check、关闭重开和幂等唯一约束。三轮“平均提交耗时”的中位数为 1,147,655 us,观测到的最慢提交为 1,263,605 us;最终空闲堆为 211,120 bytes。
EN 复位比 esp_restart() 更接近故障恢复,但仍不等于切断电源轨。真实断电、棕断、容量耗尽和长期磨损继续保留为最终验收项,README 不得把当前结果写成完整掉电认证。
子架构
Schedule / TimingTask / 其他领域
│ 各自拥有业务语义 Store Port
▼
voicelife_*_sqlite Adapter
│ 只写本领域 SQL 与行映射
▼
voicelife_storage_sqlite
├── 单数据库连接与单写者队列
├── TransactionGuard / PreparedStatement
├── schema_migrations
├── PRAGMA 配置与启动时自检
├── SQLite -> ErrorCode 映射
└── 容量、延迟和完整性指标
▼
ESP-IDF FATFS + Wear Levelling + 2 MiB 分区
边界规则:
Runtime 只创建一个数据库实例;模块 Adapter 不自行 mount、format 或重复初始化 SQLite。
跨领域原子操作由用例层定义粗粒度 Port,SQLite Adapter 在一次事务中写入业务事实、幂等记录和 Outbox;不由多个 Repository 依次提交。
设备侧采用单写者队列。语音实时任务不得直接等待约 1.1 秒的持久化提交。
生产 mount 使用 format_if_mount_failed=false。挂载或完整性检查失败时保留现场并进入受限模式,不自动格式化用户数据。
PRAGMA 必须在打开后读取回验;编译参数、分区几何和运行参数不匹配时启动失败。
默认限制单事务行数和 payload,预留数据库空间;SQLITE_FULL 映射为可恢复的容量错误,不触发自动删库。
Raw BDL VFS 只有在同一套实板契约测试和寿命评估优于 FATFS/WL 后,才能通过新 ADR 替换当前底座。
TDD 顺序
RED
GREEN
REFACTOR
验收标准
明确不做
不把 SQLite 类型放进 Domain/Application 公共接口;
不提供无业务语义的通用 CRUD 协议;
不在 mount 失败时静默格式化生产分区;
不用 esp_restart()、单次成功或 quick_check=ok 代替故障注入;
不因新 Raw BDL 原型性能更高就跳过采用度、许可、恢复和寿命验证;
不在本任务中关闭父任务 [Epic] VoiceLife 设备端 MVP 模块化开发与交付 #91 。
回退
Runtime Profile 可切回内存 Adapter,数据库分区保留不动;
迁移失败不升级 schema_version,保留旧库供恢复;
真机工具只操作经分区表解析确认的测试区域,任何摘要不匹配立即停止;
阶段性 PR 使用 Refs #本Issue 和 Refs #91,只有全部验收完成的最终 PR 才关闭本 Issue,任何 PR 都不关闭 [Epic] VoiceLife 设备端 MVP 模块化开发与交付 #91 。
结论与下一步
本任务要把“板上能执行 SQLite”升级为“板上事务行为有证据、模块共用一套数据库底座”。当前选择 SQLite 3.53.4 + ESP-IDF FATFS/Wear Levelling,固定 4 KiB 扇区、
journal_mode=DELETE、synchronous=EXTRA、psow=0;本项目实测的 LittleFS 路线不进入生产实现。下一步先合入共享 SQLite 内核与实板资格测试工具,再让 Schedule、TimingTask 等模块的 SQLite Adapter 复用同一个连接、迁移和事务边界。PR #101 中的 TimingTask SQLite 代码可作为领域 Adapter 骨架,但不能成为第二套数据库生命周期实现。
为什么现在必须统一
main上的 Schedule 骨架与 PR #101 已开始分别定义持久化模型。如果每个模块自己打开数据库、设置 PRAGMA、维护迁移和解释错误,跨 Schedule、TimingTask、Outbox、幂等记录的原子提交将无法保证,后续也会出现多套恢复策略。本任务统一的是数据库底座,不统一业务接口。领域模块继续定义有业务语义的 Store Port;不会引入
Repository<T>、字符串表名或“任意 SQL”一类通用读写协议。公开方案调查
截至 2026-08-03,没有找到一套可以直接承担 VoiceLife 生产持久化的成熟 ESP32 SQLite 方案:
siara-cc/esp32-idf-sqlite3/ Arduino 版本fsync返回值;仓库已有损坏与事务相关报告jTecRepos/jSQLite3huming2207/sqlite_espbdlunix-none+ FATFS/WL判断依据不是 star 数,而是 SQLite 对 VFS 的同步、截断、锁、扇区和删除语义有明确假设。参考:
实板证据
目标板:ESP32-S3,ESP-IDF 6.0.2,2 MiB
voicelife测试分区,串口固定115200。被否决的路线
SQLite
unix-none运行于joltwallet/littlefs 1.22.3时,以下三组配置都在显式ROLLBACK后留下了表记录,而索引记录为 0:因此,“LittleFS 自身支持掉电恢复”不能推出“SQLite 的 VFS 假设在该组合上成立”。当前测试栈明确拒绝 LittleFS + SQLite。
当前通过的路线
FATFS/WL 使用 4 KiB 扇区,SQLite 使用
DELETE + EXTRA + psow=0。主机在两个标记点通过串口控制线触发 EN 复位,设备启动原因均为rst:0x1 (POWERON):COMMIT返回后立即复位;重启后已提交标记仍存在。三轮独立测试全部通过。每轮还覆盖显式回滚、40 次四表原子提交、每次提交后的表/索引扫描一致性、
PRAGMA quick_check、关闭重开和幂等唯一约束。三轮“平均提交耗时”的中位数为 1,147,655 us,观测到的最慢提交为 1,263,605 us;最终空闲堆为 211,120 bytes。EN 复位比
esp_restart()更接近故障恢复,但仍不等于切断电源轨。真实断电、棕断、容量耗尽和长期磨损继续保留为最终验收项,README 不得把当前结果写成完整掉电认证。子架构
边界规则:
format_if_mount_failed=false。挂载或完整性检查失败时保留现场并进入受限模式,不自动格式化用户数据。SQLITE_FULL映射为可恢复的容量错误,不触发自动删库。TDD 顺序
RED
GREEN
REFACTOR
*_sqliteAdapter,Domain 和公共契约不出现sqlite3*、表名、PRAGMA 或 ESP-IDF 句柄;验收标准
voicelife_storage_sqlite的接口、依赖方向和 Runtime 组装经架构检查;voicelife分区和 OTA 元数据,并核对原有 7 条日程、8 条提醒;./scripts/run_checks.sh、ESP-IDF 6.0.2 构建和 Qiniu Runner CI 全部通过。明确不做
esp_restart()、单次成功或quick_check=ok代替故障注入;回退
schema_version,保留旧库供恢复;Refs #本Issue和Refs #91,只有全部验收完成的最终 PR 才关闭本 Issue,任何 PR 都不关闭 [Epic] VoiceLife 设备端 MVP 模块化开发与交付 #91。