# SQL 跨引擎迁移补充验收 ## 结论与范围 北京时间 2026 年 9 月 21 日,在云服务器新空数据隔离实例中,完成 MySQL → PostgreSQL、PostgreSQL → MySQL 的应用级迁移、内部重启、系统级进程重启及目标账号故障恢复。 - [✓] 保留迁移失败反例,以只读诊断定位定长字符填充差异。 - [✓] 确定性回归先失败后通过,不跳过表或降低完整性校验。 - [✓] 双 SQL 跨引擎切换、两类重启、目标不可用时拒绝启动及恢复。 - [✓] 两个方向均验证旧会话、历史邮件、新邮件、中文正文及附件。 - [✓] 源库无应用连接、源库逻辑内容不变;新增邮件仅进入目标库。 - [✓] 配置历史完整保留在目标业务库,没有生成 SQLite 文件。 - [✓] 云端新增专项零跳过;最新 Python 部署套件 136 项零跳过通过。 - [✓] 证据、源码、工具摘要、账号锁定及正式服务保护状态复核。 - [ ] 涉及 SQLite 的四个跨引擎应用级重启方向。 - [ ] SQL 复制中断、SQLite 提交及配置切换临界点恢复。 - [!] 浏览器、外部依赖、安全告警、凭据轮换及其他生产门禁尚未关闭。 **本记录只关闭明确列出的双 SQL 跨引擎项,不代表整个项目生产验收完成,也不是正式实例升级。** ## 缺陷定位与最小修复 旧候选在 `/var/tmp/smail-review-runtime-20260921-063341/` 的 MySQL → PostgreSQL 迁移中返回 HTTP 409,提示数据库数据校验未通过。目标数据复制已提交,但配置切换被拒绝;失败源库、目标库和日志均保留,没有通过重试覆盖既有目标。 只读原生 SQL 初步比较只能看到失败响应后可能变化的操作日志与会话时间,不能据此认定并发写入是原因。随后使用新建的最小 SELECT 权限账号、只读事务和生产摘要函数进行 Go 驱动级诊断,定位到 `verification_cleanup_state.lease_token`:MySQL 的定长字符读取结果与 PostgreSQL `BPCHAR` 填充后的驱动值不一致。 最终只读诊断位于 `/var/tmp/smail-review-diagnostic-20260921-070712/`。只输出表名、列名、类型、数量和差异标记,不输出行内容、凭据或摘要原文。诊断有意以非零退出报告差异,不算通过的验收。最初两类启动失败也保留:`PrivateTmp` 隐藏执行路径,以及隐藏正式家目录后驱动默认客户端证书路径不可访问。修正仅以本轮目录只读覆盖可见路径,正式 `/opt/smail` 仍被遮蔽;没有关闭 TLS、解锁原测试账号或改变正式服务。各次诊断账号均已锁定。 `backend/services/dbmigration/portable_data.go` 在行扫描后按**实际源引擎和列元数据**处理填充: - 仅 MySQL `CHAR`、PostgreSQL `BPCHAR` 删除右侧 ASCII 空格填充。 - 复制与校验共用同一读取路径,支持 PostgreSQL 作为源库的反向迁移。 - 不裁剪前导空格、制表符、换行或非断行空格。 - 不改变 VARCHAR、TEXT、BLOB、BYTEA 或 SQLite 的值,即使 SQLite 列声明为 CHAR。 - NULL 与空字符串、实际令牌内容变更、变长文本或二进制尾部差异仍须区分。 - 全表复制、行数与摘要校验、失败不切配置等保护全部保留。 ## 回归与构建 | 项目 | 结果与证据 | | --- | --- | | 修复前确定性反例 | `character-before.log`,六个扫描用例、两个摘要用例失败 | | 修复后专项 | `character-after.log`,两个顶层测试、22 个子用例通过 | | 本地迁移包 | `character-package.log`,退出零;可选 SQL 集成未配置,不能称零跳过 | | 三轮专项竞态 | `character-race.log`,退出零;同样不扩大为可选 SQL 集成通过 | | 云端新增专项 | `smail-review-ssl-apex-20260921/20260921-072942-character-fixed`,两个顶层测试、零跳过、退出零 | | 本地全量后端及 overlay | `run-2026-09-20T23-31-05-653Z-37152`,66 个测试包、1136 个顶层测试、904 个子测试通过;35 个顶层与 12 个子测试跳过,另有 18 个无测试包 | | 云端完整 Python 部署回归 | `/var/tmp/smail-review-python-20260921-072946/`,136 项、零跳过、退出零;运行 6.603 秒,内存峰值 46.7 MiB、交换零 | 全量后端的跳过包含缺少专用 SQL 连接、Windows 符号链接权限、本地管理员命名管道和浏览器夹具等条件;没有人为添加跳过或将它们计为通过。双 SQL 实机证据单独列在下节,不以这些可选测试的空运行代替。 候选重新编译后端,复用未改动的前端与 Linux 插件产物;不是本轮重新构建全部资产。构建基线为 `6f63a7e` 加本轮两个 Go 文件改动,611 个后端 Go 文件摘要保存于 `character-source-manifest.json`。 - 主程序 `smail-character`:`97ad202b67e57e6284d7707928cf4591a5d2d676344d7b67f003630242e28924`。 - Linux 测试二进制:`28e424f0a5b3663f43db55bcfd022bb3721dbb438d108184aede8b6af6d7ce96`。 - Python 部署回归日志:`c6efcb86c945ebf221652edc395133f2d454e3ef5942218591534353c61caf8d`。 ## 双向实机结果 实例完整路径前缀为 `/var/tmp/smail-review-runtime-`。 | 项目 | MySQL → PostgreSQL | PostgreSQL → MySQL | | --- | --- | --- | | 实例目录后缀 | `20260921-072157` | `20260921-072510` | | 内部重启前后 PID | `786894`,不变 | `787599`,不变 | | 系统重启后 PID | `787300` | `787953` | | 故障恢复后 PID | `787432` | `788077` | | 三阶段源/目标连接数 | 均为 `0 / 3` | 均为 `0 / 3` | | 目标账号锁定后启动 | failed,PID 为零,退出 1 | failed,PID 为零,退出 1 | | 源/目标配置历史条数 | `1 / 1`,原始载荷和摘要一致 | `1 / 1`,原始载荷和摘要一致 | | SQLite 文件数 | 零 | 零 | | OOM、资源采集及清理错误 | 全部为零 | 全部为零 | 两轮均由正式 API 预览、迁移、提交配置并触发内部重启。待重启期间拒绝业务写入,内部重启保留原会话。系统级重启后 PID 与 InvocationID 改变。锁定目标账号模拟故障时,不回退源库、不改写目标配置;恢复账号后再验登录、SMTP 收信及 IMAP/POP3 读取历史和新增邮件。 迁移历史比较使用规范化的原始字节编码,同时检查载荷 SHA256;不是只比较条数或解析后的 JSON。源库逻辑指纹在后续业务及故障注入期间保持不变。每个实例只有目标库承载活动业务和配置历史,源库只是保留的离线证据。 ## 证据与收尾复核 本地证据均位于 `test-log/production-remediation-20260921/`: - 双向汇总:`cross-sql-results-20260921-072145.json`。 - 原始证据:`cross-sql-runtime-evidence/` 下对应两个实例,共 26 份有效文件。 - 独立复核:`cross-sql-recheck-20260921-073040.json`,重新核对本地与云端摘要、六个工具及 611 个后端源码摘要。 - 全量后端统计:`character-full-backend-result.json`,保留全部跳过名称。 - 云端部署回归:`python-linux-result-20260921-072946.json` 及对应完整日志。 独立查询数据库系统表确认四个源/目标账号均锁定;两个 PostgreSQL 账号均不可登录、没有建库权限、不是超级用户。两轮桥接、应用、网络锚点及诊断单元均已停止。正式 Smail、Nginx、MySQL、PostgreSQL、Scert、FDM 的 PID、InvocationID、重启次数保持一致,正式程序摘要仍为 `c14ef1c5ba1dc2096c312c8357039a7330c985cbeac90765aed01d8151e05e1f`。 用户已有的 `docs/云端全项目测试报告.md` 未编辑、未暂存,摘要仍为 `A142FAA660487656124A8BD4A3EF4F58ED80B3DEC0EDA4AAB0F20B8C3297FF08`。失败目标、离线源库和诊断证据未删除。