本文说明当前实验实现;日常操作与排障见 中文用户手册, 英文版本见 Technical report。
tsfile_sqlite.cc 实现统一虚拟表 tsfile_hybrid,负责参数、schema、冷热读取、 热区写入、封存和事务回调。tsfile_sqlite_management.inc 位于同一匿名命名空间, 提供 seal/export 管理函数和状态、校验虚拟表。集成测试位于 ../../test/sqlite/tsfile_sqlite_test.cc,使用真实 SQLite 连接和 TsFile 读写器。
连接维护按 SQLite schema 与逻辑表名索引的表对象。每张表保存列、模式、源文件映射、 时间精度、watermark、待提交文件和 savepoint 标记。游标当前在内存中收集查询结果。
显式定义加目录、外部文件加目录、外部文件不加目录三种建表方式,最终都形成同一套 列结构,再通过 sqlite3_declare_vtab 声明给 SQLite。前两种可写,第三种只读。 文件推断只处理明确指定的 source_table,并扫描其最大时间;空源表通过完整 schema 元数据识别。普通源文件的隐含时间列名为 time,扩展生成文件使用 Property tsfile_sqlite.time_column 保存自定义时间列名。
重连从 SQLite 恢复已登记的结构,不因外部文件变化重新推断。源文件缺失时仍能查看 结构和诊断,实际读取通过文件特征检查后才继续。
| 对象 | 内容 |
|---|---|
<table>_tsfile$hot | 可写表的业务列及独立内部行号 |
<table>_tsfile$segments | 文件路径、cutoff、行数、文件内部表名及内容指纹 |
<table>_tsfile$config | 结构、模式、精度、目录、watermark、源文件及最大时间 |
<table>_tsfile$key | 热数据的逻辑唯一键索引 |
只读表不创建 hot。内部对象与逻辑表处于相同 SQLite schema,名称全部转义;冲突时 拒绝创建。schema 以带长度的列名、TsFile 类型和类别序列保存,不因 SQLite 类型映射 丢失原类型信息。直接修改内部对象不属于受支持接口。
每个 TAG 在唯一索引中使用 (tag IS NULL) 和 coalesce(tag,'') 两个分量,最后 追加 TIME。这使 NULL 键相互冲突,同时保留 NULL 与空字符串的区别。无 TAG 时只有 TIME 键。源文件原有重复行保持原样,新数据必须晚于源表最大时间,不能覆盖历史。
热区内部行号选择不与业务列冲突的 tsfile$rowid 名称,业务 rowid/oid/rowid 不影响 内部定位。冷行使用负的合成行号,仅用于操作内定位,不是稳定业务主键。
xBestIndex 提供整数时间范围、BINARY TAG 等值和列裁剪信息。xFilter 读取热区及登记 冷文件,并使用每段保存的 source_table 映射;事务内新封存的段从本次临时路径读取。 支持的时间和 TAG 条件下推给 TsFile,剩余谓词、关联、聚合、排序和分页由 SQLite 执行,结果顺序需要显式 ORDER BY。
xUpdate 检查类型、TIME 非空、唯一键和 watermark,只写入热区。实际命中冷行的 UPDATE/DELETE,以及只读表实际写入,返回非约束类错误 SQLITE_READONLY,保证 IGNORE/FAIL 也不能留下同一语句的部分热区修改。SQLite 不调用零行 DML 的 xUpdate, 所以只读表上的无目标行操作可以作为空操作成功。
watermark 是可接受新时间的闭下界。普通空表从 INT64_MIN 开始;有源数据时为 source_max + 1。可写表的 NULL watermark 表示时间空间耗尽,与只读表通过 mode 区分。 xBegin 重新加载 SQLite 中的边界,避免另一连接封存后仍使用旧缓存允许迟到写入。
可写目录在建表时必须不存在或为空,不能与其他自有目录重合、嵌套或包含源文件。 扩展解析已有祖先的真实路径,以独占创建并 fsync 的 .tsfile-owner 记录归属。 标记绑定 SQLite 数据库规范路径及表名;内存数据库使用连接身份。改变 ATTACH 别名 不改变归属,但复制或移动数据库不能直接复用原可写目录。重连和写事务检查归属。
失败或回滚建表仅回收本次创建且拥有的标记和空目录。DROP 保留已提交文件及标记, 避免事务回滚后失去目录归属;后续清理由调用方负责。
内容特征使用文件大小和遍历全部字节的 FNV-1a 指纹,用于检测变化,不提供密码学 完整性证明。外部源文件必须由调用方保持路径和内容不变。扩展不因文件后缀就删除 未知 .tmp 或 .tsfile,也不把源文件父目录当作自己拥有的目录。
管理函数只接受独立顶层 SELECT。seal 创建内部 savepoint,并通过虚拟表的零行 UPDATE 使 SQLite 注册该表的写事务回调;随后直接执行封存。因此封存参与同一连接 的外层事务及 savepoint。连接上的管理重入保护阻止嵌套管理操作。
非空封存流程如下:
空封存只推进边界,不生成文件。显式 cutoff 为 int64 半开上界;export 自动封存 可以额外覆盖 INT64_MAX 并将可写时间标记为耗尽。
xRollback 只删除本次操作拥有的临时或已发布文件。xSavepoint 按 SQLite savepoint ID 记录待提交文件数,xRollbackTo 删除其后的文件并恢复缓存配置,xRelease 移除释放的 标记。回滚跨越新建表时,允许 SQLite 已删除 config,并释放本次新目录资源。
macOS 使用 renamex_np 的 RENAME_EXCL,Linux 使用 renameat2 的 RENAME_NOREPLACE。 已有目录项,包括悬空符号链接,均不会被替换。不支持该操作的文件系统或内核会报错。
文件持久化先于 SQLite 提交文件引用。进程在 SQLite 提交前中断可能留下未登记文件, 但 SQLite 仍保留旧热行,该文件不参与查询。verify 可以报告未登记的 TsFile,重连 不会猜测归属并自动删除。持久性依赖 SQLite journal/synchronous 设置和文件系统 正确执行 fsync,这不是跨文件系统的分布式事务。
export 禁止放入用户显式事务,先检查目标路径,再在内部事务中验证并捕获冷数据, 对可写表注册写事务后读取热数据。SQLite 的快照和写锁使并发冲突明确失败,不会让 一次导出静默混入边界检查之外的写入。
本次所有热行自动封存并提交后,将捕获的完整数据排序、重写到独立 staging 目录。 非空输出为一个 part-000001.tsfile,空表输出空目录。输出只含所选逻辑表,文件内部 表名使用逻辑表名,保留已知时间精度。源文件的其他表不会被带出。
输出完成后 fsync staging,以不覆盖方式原子发布目标目录,再 fsync 父目录。 封存提交与输出发布为两个阶段;第二阶段失败不会把冷数据变回热数据,也不会回退 watermark。错误说明封存是否已提交;父目录 fsync 失败时还会说明完整输出已发布。 普通失败清理本次已知 staging 文件,进程中断可以留下隔离目录,不发布部分结果。 封存提交后的新写入不进入本次输出,仍保留在热区。
tsfile_table_info 从当前事务视图读取配置、热行数和文件数。tsfile_verify 检查 登记文件是否存在、Reader 是否可解析元数据、指定表 schema 是否存在,以及全文 指纹是否一致;不逐页解码所有数据。它只扫描自有目录当前层的普通 .tsfile,不跟随 扫描项的符号链接,也不递归或扫描外部源文件父目录。状态为 OK、MISSING、CORRUPT、 MISMATCH、UNREGISTERED,仅报告,不注册、修复或删除文件。
集成测试覆盖三种建表、NULL 逻辑键、无 TAG、源表映射、空及多表文件、精度、INT64 耗尽、业务 rowid 列、冷热写约束、事务/savepoint、重连、并发边界、源文件变化与缺失、 目录归属、自动导出和标准 Reader 回读。回归还覆盖封存已提交后的导出失败、回滚跨越 建表、复制数据库抢占原目录,以及发布时已有目录项。
额外子进程检查在 DELETE/WAL 两种 journal 模式下,封存提交前后直接退出并重新打开 数据库,验证数据与冷热状态。这些检查不代表每个断电或文件系统故障点都已验证。 构建及测试命令见 用户手册。
查询、封存及导出会在内存中收集数据,导出排序并重写完整快照,文件指纹也需要完整 读取文件;当前尚不是限制内存用量的流式实现。早期原型的 column=、隐藏管理列及 内部表布局已替换,不自动迁移原型数据库。首版不提供后台封存、compaction、自动 保留期、历史修正、批量文件注册、原地 schema 演进或完整数据库备份恢复。 Linux 特有文件发布路径需要 Linux CI 验证;本地验证环境是 macOS 和 Homebrew SQLite。