SQLite FTS5 跨架构迁移踩坑记:从 x86 到 ARM64

分钟
SQLite FTS5 跨架构迁移踩坑记:从 x86 到 ARM64

SQLite FTS5 跨架构迁移踩坑记:从 x86 到 ARM64

背景

最近将 Hermes Agent(一个基于 SQLite 的 AI 聊天代理)从一台 x86_64 Arch Linux 服务器迁移到一台 ARM64 Armbian 开发板上。数据量不大:约 111MB 的 state.db,包含 9786 条消息、186 个会话。

迁移数据库嘛,听起来很简单对吧?然而这次踩了一个大坑。

迁移方案的选择

方案一:.dump / .reload(我选的,后来证明是错的)

# 在源机器上导出
sqlite3 state.db ".dump" > dump.sql

# 在目标机器上导入
sqlite3 state.db < dump.sql

看起来很标准对吧?SQLite 官方文档也推荐这个方法来做版本升级或格式转换。但问题是——当数据库包含 FTS5 虚拟表时,这个方法有严重的隐患。

方案二:直接复制 .db 文件(正确方案)

# 直接拷贝
scp user@source-server:/path/to/state.db ./state.db

没错,就这么简单。SQLite 的 .db 文件是二进制跨架构兼容的——x86 和 ARM64 使用相同的字节序(小端),SQLite 文件格式不依赖任何平台特定的结构。直接复制就行。

问题现象

使用 .dump / .reload 导入后,数据库看起来正常,表和数据都在。但当 Agent 尝试写入新消息时:

RuntimeError: constraint failed

具体报错指向 FTS5 虚拟表的插入操作。进一步测试:

-- 查询没问题
SELECT COUNT(*) FROM messages;  -- 9786,正常

-- 插入就报错
INSERT INTO messages (session_id, role, content, timestamp)
VALUES ('test', 'user', 'hello', datetime('now'));
-- Error: constraint failed

诊断过程

1. 标准完整性检查——没用

PRAGMA integrity_check;
-- ok

PRAGMA integrity_check 返回正常!但它不会检查 FTS5 shadow 表的完整性。这是一个容易被忽略的坑。

2. FTS5 专用完整性检查——发现问题

FTS5 提供了内置的完整性检查命令:

SELECT * FROM messages_fts('integrity-check');
  • 如果 FTS5 索引完好,这条语句返回空结果集
  • 如果索引损坏,会返回错误描述

在我的案例中,它返回了不一致的错误信息,确认 FTS5 索引已损坏。

3. 检查 shadow 表

FTS5 虚拟表会在底层创建一组 “shadow 表”(以 _data_idx_content_docsize_config 为后缀)。你可以直接查看:

-- 查看有哪些 shadow 表
SELECT name FROM sqlite_master
WHERE type='table' AND name LIKE 'messages_%';

这些表的状态在 dump/reload 后可能不一致。

根因分析

问题出在 .dump 导出的 SQL 语句顺序上。

.dump 会导出:

  1. CREATE VIRTUAL TABLE messages_fts USING fts5(...) — 创建 FTS5 表
  2. 各种 INSERT 语句填充主表
  3. 可能还有触发器(用于自动同步主表和 FTS5 索引)

但在 reload 时,触发器可能会在插入主表时同时触发 FTS5 索引更新,而这些更新与后续的 shadow 表数据发生冲突。在不同架构间,SQLite 内部的某些排序或序列化行为可能有微小差异,进一步加剧了这种不一致。

另外还发现 sqlite_sequence 表的值严重异常——autoincrement 计数器显示约 3 万亿,而实际只有不到 15000 行。这也是 dump/reload 过程中的副作用。

修复方案

完整修复脚本

以下是一个通用的 FTS5 索引修复脚本:

#!/usr/bin/env bash
# repair_fts.sh - 修复 FTS5 索引
# 用法: bash repair_fts.sh <数据库路径> <FTS表名> <主表名> <内容列>

set -euo pipefail

DB="${1:?用法: bash repair_fts.sh <db_path> <fts_table> <main_table> <content_columns>}"
FTS_TABLE="${2:?请指定 FTS5 虚拟表名}"
MAIN_TABLE="${3:?请指定主表名}"
CONTENT_COLS="${4:?请指定内容列名,如 'role, content'}"

BACKUP="${DB}.bak.$(date +%Y%m%d%H%M%S)"

echo "=== SQLite FTS5 索引修复工具 ==="
echo ""
echo "数据库: $DB"
echo "FTS表:  $FTS_TABLE"
echo "主表:   $MAIN_TABLE"
echo "列:     $CONTENT_COLS"
echo ""

# 1. 备份
echo "[1/5] 备份数据库..."
cp "$DB" "$BACKUP"
echo "  备份已保存到: $BACKUP"
echo ""

# 2. 诊断
echo "[2/5] 诊断当前状态..."
echo "  主表行数:"
sqlite3 "$DB" "SELECT COUNT(*) FROM $MAIN_TABLE;"
echo "  FTS5 完整性检查:"
RESULT=$(sqlite3 "$DB" "SELECT * FROM ${FTS_TABLE}('integrity-check');")
if [ -z "$RESULT" ]; then
    echo "  ✓ FTS5 索引完好"
    read -p "  索引看起来正常,是否仍要继续?(y/N) " -n 1 -r
    echo
    [[ $REPLY =~ ^[Yy]$ ]] || exit 0
else
    echo "  ✗ 发现问题: $RESULT"
fi
echo ""

# 3. 删除旧 FTS 表及其相关对象
echo "[3/5] 删除旧的 FTS 表..."
sqlite3 "$DB" <<SQL
-- 删除触发器(如果存在)
SELECT 'DROP TRIGGER IF EXISTS ' || name || ';'
FROM sqlite_master WHERE type='trigger' AND tbl_name='$MAIN_TABLE'
AND (name LIKE '%fts%' OR name LIKE '%ai%' OR name LIKE '%ad%' OR name LIKE '%au%');

-- 列出将被删除的相关表
SELECT '  将删除: ' || name FROM sqlite_master
WHERE type='table' AND name LIKE '${FTS_TABLE}%';
SQL

sqlite3 "$DB" <<SQL
DROP TABLE IF EXISTS $FTS_TABLE;
DROP TABLE IF EXISTS ${FTS_TABLE}_data;
DROP TABLE IF EXISTS ${FTS_TABLE}_idx;
DROP TABLE IF EXISTS ${FTS_TABLE}_content;
DROP TABLE IF EXISTS ${FTS_TABLE}_docsize;
DROP TABLE IF EXISTS ${FTS_TABLE}_config;
SQL
echo "  ✓ 已删除"
echo ""

# 4. 重建 FTS 表和索引
echo "[4/5] 重建 FTS 表..."
sqlite3 "$DB" <<SQL
CREATE VIRTUAL TABLE IF NOT EXISTS ${FTS_TABLE} USING fts5($CONTENT_COLS, content='$MAIN_TABLE', content_rowid='id');
INSERT INTO ${FTS_TABLE}(${FTS_TABLE}) VALUES('rebuild');
SQL
echo "  ✓ 已重建"
echo ""

# 5. 验证
echo "[5/5] 验证..."
RESULT=$(sqlite3 "$DB" "SELECT * FROM ${FTS_TABLE}('integrity-check');")
if [ -z "$RESULT" ]; then
    echo "  ✓ FTS5 索引已修复!"
    COUNT=$(sqlite3 "$DB" "SELECT COUNT(*) FROM $MAIN_TABLE;")
    echo "  ✓ 索引覆盖 $COUNT 条记录"
else
    echo "  ✗ 仍有问题: $RESULT"
    echo "  请从备份恢复: cp $BACKUP $DB"
    exit 1
fi

echo ""
echo "=== 修复完成 ==="

使用方法

# 赋予执行权限
chmod +x repair_fts.sh

# 针对 messages 表修复
bash repair_fts.sh ./state.db messages_fts messages "role, content"

关键步骤解释

步骤说明
备份修复前必须备份,万一出问题可以回退
诊断用 FTS5 的 integrity-check 确认问题
删除彻底清除损坏的 FTS5 表和 shadow 表
重建content= 参数关联主表,rebuild 命令重建索引
验证再次运行 integrity-check 确认修复成功

修复 sqlite_sequence

如果 autoincrement 计数器也异常了:

-- 查看当前值
SELECT * FROM sqlite_sequence;

-- 重置为实际最大值
UPDATE sqlite_sequence SET seq = (SELECT MAX(id) FROM messages) WHERE name = 'messages';

-- 或者直接删除,下次插入时会自动重新生成
DELETE FROM sqlite_sequence WHERE name = 'messages';

最终状态

修复完成后,数据库恢复正常:

数据库大小:  111.4 MB
消息总数:    9786
会话数:      186
FTS5 索引:   ✓ 健康
完整性检查:  ok

经验总结

核心教训

SQLite + FTS5 跨架构迁移,永远不要用 .dump / .reload。直接复制 .db 文件。

SQLite 数据库文件是二进制兼容的(前提是字节序相同,x86 和 ARM64 都是小端)。直接 scprsync 就行,不需要转成 SQL 文本再导入。

如果你必须用 .dump

有时候你确实需要文本格式(比如要合并两个数据库、或者要跨字节序迁移),那么:

  1. 导出时排除 FTS5 表:先 dump 除 FTS 相关表以外的所有内容
  2. 导入主表数据
  3. 在目标数据库上重新创建 FTS5 虚拟表
  4. 运行 INSERT INTO fts_table(fts_table) VALUES('rebuild'); 重建索引
  5. SELECT * FROM fts_table('integrity-check'); 验证

FTS5 完整性检查速查

-- 常规检查(不检查 FTS5!)
PRAGMA integrity_check;

-- FTS5 专用检查(这才是正确的)
SELECT * FROM your_fts_table('integrity-check');
-- 返回空 = 正常
-- 返回内容 = 有问题

迁移前检查清单

  • 备份原始数据库
  • 确认迁移方式(推荐直接复制文件)
  • 迁移后运行 PRAGMA integrity_check;
  • 如果有 FTS5,运行 SELECT * FROM fts_table('integrity-check');
  • 测试读写操作
  • 检查 sqlite_sequence 值是否合理

本文记录于 2026-06-11,基于 Hermes Agent 从 x86_64 Arch Linux 迁移到 ARM64 Armbian 的实际经历。希望对遇到类似问题的朋友有所帮助。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

SQLite FTS5 跨架构迁移踩坑记:从 x86 到 ARM64
https://foxlesbiao.github.io/blog/posts/sqlite-fts5-cross-architecture-migration/
作者
Illya-Lily-Iris·Alice
发布于
2026-06-10
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Illya-Lily-Iris·Alice
在数字与现实之间反复横跳
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签
站点统计
文章
7
分类
2
标签
28
总字数
13,176
运行时长
0
最后活动
0 天前
站点信息
构建平台
GitHub Actions
博客版本
Firefly v6.15.4
文章许可
CC BY-NC-SA 4.0