tsfile_sqlite 用户手册

tsfile_sqlite 是一个实验性的 SQLite loadable extension。它提供 tsfile_hybrid 虚拟表,让一张逻辑表同时使用两种物理存储:

  • 尚未封存的近期数据保存在 SQLite shadow table 中,支持事务和 CRUD;
  • 已封存的历史数据保存在不可变的 TsFile 段文件中;
  • 应用继续对同一张虚拟表执行 SQL,扩展自动合并冷热数据。

它适合以追加为主、近期数据偶尔需要修正、历史数据可以冻结的时序场景。 它不是 SQLite 通用表的替代存储引擎,也不会自动把已有 SQLite 表转换为 TsFile。

1. 环境要求

  • Linux 或 macOS;
  • SQLite 3.31 或更高版本;
  • 支持加载扩展的 SQLite 构建;
  • CMake 构建时启用共享版 libtsfile;
  • TsFile 目录必须使用绝对路径,并由一张逻辑表独占。

当前 MVP 不支持 Windows。

2. 构建

在仓库根目录执行:

cmake -S cpp -B cpp/build/sqlite \
  -DBUILD_SQLITE_EXTENSION=ON \
  -DTSFILE_BUILD_SHARED=ON \
  -DBUILD_TEST=ON

cmake --build cpp/build/sqlite --target tsfile_sqlite -j

产物位于构建目录的 lib 子目录:

  • Linux:cpp/build/sqlite/lib/tsfile_sqlite.so
  • macOS:cpp/build/sqlite/lib/tsfile_sqlite.dylib

扩展依赖同一次构建产生的 libtsfile。默认 RPATH 会从扩展所在目录寻找 libtsfile,部署时建议把二者放在同一目录。

运行扩展测试:

cmake --build cpp/build/sqlite --target TsFile_Sqlite_Test -j
ctest --test-dir cpp/build/sqlite/test -R TsFileSqliteTest \
  --output-on-failure

3. 加载扩展

3.1 SQLite CLI

.load /absolute/path/to/tsfile_sqlite

SQLite CLI 通常会根据平台自动补全 .so 或 .dylib 后缀。也可以传入完整 文件名。

3.2 C/C++ 应用

sqlite3_enable_load_extension(db, 1);

char *error = NULL;
int rc = sqlite3_load_extension(
    db, "/absolute/path/to/tsfile_sqlite", NULL, &error);

sqlite3_enable_load_extension(db, 0);

应用应在打开数据库连接后、访问 hybrid 表之前加载扩展。生产环境建议加载 完成后立即关闭动态扩展加载能力。

4. 创建逻辑表

以下按目标功能设计定义建表语法;示例表达待实现接口,不代表当前原型已支持。 列定义采用 列名 类型 [类别],省略类别时默认为 FIELD;TIME 和 TAG 显式声明。

CREATE VIRTUAL TABLE sensor USING tsfile_hybrid(
  time TIMESTAMP TIME,
  device STRING TAG,
  region STRING TAG,
  temperature DOUBLE FIELD,
  status STRING FIELD,
  payload BLOB FIELD,
  directory='/var/lib/example/sensor',
  timestamp_precision='ms'
);

列定义按书写顺序组成 schema,表级选项使用 key=value。未知选项、重复的表级 选项和不合法的列定义在建表时返回明确错误。标识符支持双引号转义,例如 "sensor value" DOUBLE;字符串选项使用单引号。

模块参数如下:

参数要求
directory必填、绝对路径、由当前逻辑表独占
timestamp_precision必填,只能是 ms、us 或 ns
column可重复,格式为 名称:类型:类别

列定义的目标规则如下:

  • 恰好一个 TIME 列,必须是第一列,类型为 TIMESTAMP,值不能为 NULL;
  • TAG 列可以有零个或多个;存在时类型必须为 STRING,值允许为 SQL NULL;
  • 有 TAG 时,全部 TAG 与 TIME 共同组成唯一键;无 TAG 时,TIME 单独组成唯一键;
  • 不声明类别的列默认为 FIELD,FIELD 值可以为 NULL;
  • 列名不能仅靠 ASCII 大小写区分,例如 Temperature 和 temperature 视为重名;
  • 封存通过第 7 节的管理 UDF 发起,业务 schema 不需要声明或操作 _tsfile_command、_tsfile_cutoff 控制列。

NULL TAG 的目标语义:

  • SQL NULL、空字符串 '' 和字符串 'null' 是三个不同值,封存和查询必须保留区别;
  • 判定逻辑唯一键时,相同位置的两个 NULL TAG 视为同一个键分量。例如同一 TIME 下, 两条 (device=NULL, region='cn-east') 记录冲突;
  • NULL 查询使用 IS NULL,普通 WHERE 表达式继续遵循 SQLite 的 NULL 语义, 不把 = NULL 改成相等比较;
  • 热数据的唯一键检查必须显式处理 NULL,不能仅依赖 SQLite 默认 UNIQUE 对 NULL 的处理,也不能用可能与实际 TAG 冲突的字符串替换 NULL。

无 TAG 表可按以下方式定义;每个时间戳最多对应一行:

CREATE VIRTUAL TABLE readings USING tsfile_hybrid(
  time TIMESTAMP TIME,
  value DOUBLE,
  directory='/var/lib/example/readings',
  timestamp_precision='ms'
);

所有 TAG 列与 TIME 列共同组成唯一键。例如上表的唯一键是:

(device, region, time)

4.1 数据类型映射

TsFile 类型SQLite 表现写入要求
BOOLEANINTEGER必须传 SQLite INTEGER;0 为假,非 0 为真
INT32INTEGER必须在 int32 范围内
INT64INTEGERSQLite int64
FLOATREALINTEGER 或 REAL
DOUBLEREALINTEGER 或 REAL
TEXTTEXTSQLite TEXT
STRINGTEXTSQLite TEXT
BLOBBLOBSQLite BLOB,保留长度和二进制零字节
DATEINTEGER必须在 int32 范围内
TIMESTAMPINTEGERSQLite int64

扩展不会换算时间戳。timestamp_precision 仅声明整数时间戳的单位,并写入 非空 TsFile 段的 tsfile_sqlite.timestamp_precision property。

4.2 创建后的固定配置

schema、目录和时间精度会记录在配置 shadow table 中。数据库重新打开时, 扩展会验证这些信息是否与 CREATE VIRTUAL TABLE 中保存的参数一致。

当前版本不支持修改 schema、目录或时间精度。需要变更时,应创建一张新的 逻辑表并迁移数据。

5. 写入和修改热数据

普通 DML 的用法与 SQLite 表一致:

INSERT INTO sensor(time, device, region, temperature, status)
VALUES (1700000000000, 'device-1', 'cn-east', 21.5, 'ok');

UPDATE sensor
SET temperature = 22.0
WHERE device = 'device-1'
  AND region = 'cn-east'
  AND time = 1700000000000;

DELETE FROM sensor
WHERE device = 'device-1'
  AND region = 'cn-east'
  AND time = 1700000000000;

热数据实际写入 <虚拟表名>_data shadow table,因此自动使用 SQLite 的 rollback journal/WAL、锁、唯一约束、事务和 savepoint。

BEGIN;

INSERT INTO sensor(time, device, region, temperature)
VALUES (1700000001000, 'device-1', 'cn-east', 22.1);

UPDATE sensor
SET status = 'checked'
WHERE device = 'device-1' AND time = 1700000001000;

COMMIT;

6. 查询冷热数据

无论数据位于 SQLite 还是 TsFile,都查询同一张虚拟表:

SELECT time, device, temperature, status
FROM sensor
WHERE device = 'device-1'
  AND time >= 1700000000000
  AND time < 1700086400000
ORDER BY time;

常规 SQLite SQL 仍然可用,包括 FIELD 条件、表达式、聚合、排序和分页:

SELECT device, avg(temperature)
FROM sensor
WHERE time >= 1700000000000
  AND temperature IS NOT NULL
GROUP BY device;

为了得到更好的 TsFile 扫描效率,查询应尽量包含:

  • 整数时间范围;
  • 使用 BINARY collation 的 TAG 等值条件;
  • 只选择需要的列。

扩展会把这些条件和投影下推到冷热读取路径。FIELD 条件、排序、聚合、 LIMIT/OFFSET 由 SQLite 在合并结果上处理。扩展不会声称原始输出已经满足 ORDER BY,因此需要稳定顺序时必须显式写出 ORDER BY。

所有已下推的约束仍由 SQLite 二次检查,以保证 SQL 结果正确。

7. 封存历史数据

使用两个隐藏列发送 seal 命令:

INSERT INTO sensor(_tsfile_command, _tsfile_cutoff)
VALUES ('seal', 1700086400000);

cutoff 是不包含的上界。上述操作封存:

旧 watermark <= time < 1700086400000

其中 time == 1700086400000 的行仍在热区,可以继续修改。

一次非空 seal 会:

  1. 按全部 TAG、TIME 排序读取待封存热数据;
  2. 使用 Tablet 批量写入临时 TsFile;
  3. 校验并将临时文件原子改名为 .tsfile;
  4. 在 manifest 中登记文件;
  5. 从热表删除已封存行;
  6. 将 watermark 推进到 cutoff。

如果区间内没有数据,不生成 TsFile,但仍会推进 watermark。cutoff 不能小于 当前 watermark。

seal 是同步写操作,在完成期间会占用 SQLite 写事务。可以显式把它放入事务:

目标设计使用显式管理 UDF 封存数据:

SELECT tsfile_seal('main.sensor', 1700086400000);

tsfile_seal(table_name, cutoff) 的接口契约:

  • table_name 指定一张 hybrid 逻辑表,示例中为 main schema 下的 sensor;
  • cutoff 必须为整数,单位与该表的 timestamp_precision 相同;
  • 封存 [当前 watermark, cutoff),等于 cutoff 的行继续留在热区;
  • cutoff 小于当前 watermark 时返回约束错误,等于 watermark 时返回 0;
  • 成功返回本次封存的行数。空区间返回 0,但 cutoff 更大时仍推进 watermark;
  • 取得写权限后重新读取 watermark。封存与普通写入使用同一事务协调;
  • 自动提交模式下,独立语句完成时提交;显式事务内不擅自提交外层事务, 返回行数仅表示本事务已执行封存,最终持久化取决于外层 COMMIT;
  • 失败时撤销本次调用的元数据、热数据删除和待发布文件,不能部分封存。

也可以由调用方显式控制事务:

BEGIN IMMEDIATE;
SELECT tsfile_seal('main.sensor', 1700086400000);
COMMIT;

该 UDF 属于有副作用的管理操作,规定以独立顶层 SELECT 调用,不支持放入 逐行查询、视图、触发器或其他 schema 表达式。注册时不标记为 deterministic, 并限制间接调用。业务接口不再要求向隐藏控制列插入数据。

如果事务回滚,manifest、watermark 和热数据删除都会回滚,扩展也会删除本次 事务产生的临时文件或已经改名的段文件。

8. Watermark 和冷数据不可变性

watermark 把逻辑时间轴分成两部分:

time < watermark     冷区:不可修改
time >= watermark    热区:允许 INSERT/UPDATE/DELETE

以下操作会返回约束错误:

  • 插入 time < watermark 的行;
  • 把热行的时间更新到 watermark 之前;
  • 更新或删除已经位于 TsFile 的冷行;
  • 写入重复的 (所有 TAG, TIME) 唯一键。

当前版本没有 correction 或 tombstone。如果业务必须修正历史数据,需要重建 逻辑表或在业务层保留单独的修正数据。

9. 内部状态与诊断

每张名为 sensor 的 hybrid 表拥有三个 SQLite shadow table:

表内容
sensor_data可变热数据
sensor_segmentsTsFile 路径、cutoff 和行数 manifest
sensor_configwatermark、精度、目录和 schema 签名

可以只读查看它们进行诊断:

SELECT watermark, precision, directory FROM sensor_config;

SELECT path, cutoff, row_count
FROM sensor_segments
ORDER BY cutoff;

SELECT count(*) AS hot_rows FROM sensor_data;

内部对象采用带符号的命名形式 "<逻辑表名>_tsfile$<用途>"。例如 sensor 对应:

内部表用途
"sensor_tsfile$hot"可变热数据
"sensor_tsfile$segments"TsFile 文件登记信息
"sensor_tsfile$config"schema、精度、watermark 和目录配置

名字必须统一做标识符转义。建表前检查全部派生名;任何同名对象都使创建失败并明确 报告冲突,不覆盖或复用用户对象。符号用于提高辨识度,不能被当作绝不撞名的保证。 最后一个下划线之前保留完整逻辑表名,以维持 SQLite 的 shadow 表识别关系。

这些名字属于内部实现,不作为业务 API。日常诊断通过公开状态接口完成;用户能够 在 schema 中查看内部对象,但不应直接修改它们。绕过虚拟表写入会破坏 watermark、 文件登记和实际文件之间的一致性。

热行使用 SQLite 的正 rowid;冷行使用扩展生成的负 rowid。冷 rowid 是内部 实现标识,不应作为跨查询或跨版本稳定的业务主键。

10. 数据目录和生命周期

每张逻辑表必须使用独占目录。扩展会在 seal 前清理目录中未被 manifest 引用的 .tmp 和 .tsfile 文件,用来恢复进程崩溃后遗留的孤儿文件。因此不要把手工 创建的 TsFile、其他表的段文件或任何同后缀文件放进该目录。

执行:

DROP TABLE sensor;

会删除虚拟表及其三个 shadow table,但不会删除已经导出的 TsFile。删除或归档 这些文件需要由运维流程显式完成。

11. TsFile 导出、导入与文件校验(功能设计草案)

本节定义面向标准 TsFile 的交换和查询接口。导出产物只有 .tsfile 文件,不导出 SQLite 数据库、热表、WAL、shadow 表或独立的 JSON 清单,也不承诺恢复源表的可写状态。 导入接口及只读语义在 11.2 节定义。

11.1 仅导出 TsFile

SELECT tsfile_export('main.sensor', '/export/sensor-001');

tsfile_export(table_name, output_directory) 导出指定逻辑表已经落到 TsFile 的 数据,成功返回产出的文件数。SQLite 中尚未封存的热数据不在导出范围内。 如需导出这些热数据,调用方先显式 seal 到选定 cutoff,提交后再执行 export。 export 本身不封存、不修改源数据、不推进 watermark。

目标目录必须为绝对路径且尚不存在,并且不得与源数据目录重合或互相嵌套。 输出示例:

sensor-001/
  part-000001.tsfile
  part-000002.tsfile
  • 表名、列类型和类别从 TsFile 自身元数据读取;可用的时间精度放在文件 Properties。
  • 输出文件只包含所选逻辑表的数据。如果输入段同时含其他表,需要导出所选表为独立 TsFile,不能直接复制而意外带出其他表。文件内部表名使用该逻辑表的导出名称。
  • 文件可由标准 TsFile Reader 独立读取,不依赖本扩展的 JSON 清单或 SQLite 文件。
  • 没有已封存数据时返回 0,目标目录为空;不会制造可恢复空表或热数据的假象。

第一版采用保守的一致性方式:取得源 SQLite 数据库的写保留锁,在同一事务视图内 固定该表的文件登记集合,复制或重写完成后释放锁。期间阻塞该数据库的其他写入。 对外部只读文件也要检查读取错误和文件变化;外部修改不属于 SQLite 锁的保护范围。

先在目标旁的专用暂存目录生成文件,校验 footer、schema 和已知精度,完成文件同步, 再原子发布整个目录并同步父目录。失败不发布完整目标,源文件不变。 仅允许独立管理调用,不接受用户已有的显式事务;成功返回时输出目录已发布。

11.2 扫描目录并建立只读查询表

SELECT tsfile_import('/data/archive');

tsfile_import(directory [, target_schema]) 默认把发现的表注册到 main;可显式 指定一个已经存在的 SQLite schema。成功返回新注册的逻辑表数量。

SELECT tsfile_import('/data/archive', 'archive');

这里的导入指登记外部 TsFile 并建立查询入口:默认直接引用原文件,不复制、移动、 重写文件,也不把行装入 SQLite 热表。源目录必须是可持续访问的绝对路径,文件需 由调用方保持不可变。SQLite 中只保存查询所需的文件登记和 schema 元数据。

扫描与分组规则:

  1. 第一版扫描指定目录当前层的普通 .tsfile 文件,不递归子目录,不跟随符号链接; 非 TsFile 文件忽略,扩展名匹配但无法读取的文件使本次导入失败并报告路径。
  2. 从每个文件的元数据枚举全部表名;一个文件包含多张表时分别登记,不按文件名猜 表名。同一张表出现在多个文件中时,组成同一个只读逻辑表。
  3. 同名表要求列定义、类型、类别和顺序兼容;第一版要求完全一致,不自动补列、 类型提升或统一不同 schema。按 SQLite 标识符比较产生的大小写冲突应明确报错。
  4. 接受普通 TsFile,不要求来自本扩展,也不要求携带私有快照清单。时间精度属性 存在时读取;缺失时标记为 unknown 并按原始整数时间查询,不默认为 ms。 同名表的已知精度必须一致;已知/未知混合也拒绝自动合并,避免混淆时间单位。
  5. 同名表跨文件的结果按 UNION ALL 语义读取,重叠时间和重复逻辑键保留,不自动 覆盖、去重或取最新值。只读导入不对外部数据强加可写 hybrid 表的唯一键约束。
  6. 保存“SQLite 逻辑表 → 文件路径集合 → 各文件内部表名”的映射,所有路径和表名 都按数据处理并正确转义。文件内部表名不会因 SQL 名称变化而被重写。

用户通过普通 SQL 查询,例如目录内包含 sensor 和 meter 两个表时:

SELECT time, device, temperature FROM sensor ORDER BY time;
SELECT count(*) FROM meter;

所有导入表默认且固定为只读:拒绝 INSERT、UPDATE、DELETE 和 seal,即使当前表为空 也不接受写入。只读表不创建可写热区,不恢复源库 watermark,也不因为时间较新就 自动允许修改。持续追加写入仍使用另行创建的可写 hybrid 表。

注册时先完整枚举、验证并建立映射,再在同一个 SQLite 事务中发布所有表。目标 schema 中任何同名用户表、虚拟表或所需内部对象冲突,都使整批失败;不覆盖、不 合并到已存在的表。对同一目录重复调用也遵循此规则,不会重复追加登记。

导入要求独立管理调用,不能嵌入已有用户显式事务。取得目标写锁后再次检查对象 冲突;失败回滚本次新建的所有登记,源文件保持不变。空目录或未发现表时返回 0。 本次扫描后新加入目录的文件不会自动进入已注册表,第一版不提供后台监控或刷新; 如需重新登记,可删除相关只读逻辑表后再导入,删除逻辑表不会删除外部文件。

11.3 文件所有权、移动与校验

SELECT * FROM tsfile_verify('main.sensor');

tsfile_verify(table_name) 是只读表值接口,返回 segment_id, path, status, detail。状态包括 OK、MISSING、CORRUPT、 MISMATCH 和 UNREGISTERED。导入时建立文件指纹,verify 对比文件内容、schema 和可用精度;普通文件不需要预先存储本扩展的校验属性。

  • 自有 hybrid 段与外部只读引用必须有明确的所有权区别。导入不取得源文件的删除权, DROP TABLE、seal 清理和失败恢复都不得删除外部引用文件。
  • 文件被移走或删除时,访问该文件的查询失败并报告表名、文件标识和原路径,不能 静默跳过;verify 可集中报告问题,不承诺持续监控或自动搜索新路径。
  • 同路径文件被替换或修改时,完整 verify 对比导入指纹并报告差异;普通查询的轻量 检查不宣称能够发现所有字节修改。调用方必须保证登记后外部文件保持不可变。
  • 外部目录新增文件不会自动导入;verify 报告发现的未登记 TsFile,且不删除它们。 自有目录中的未知文件也不能仅凭 .tsfile 后缀被孤儿清理删除。
  • 复制到其他位置且保留源文件不影响已有查询;移动目录后应在保留源数据的前提下 重新建立登记,不直接改写内部表路径。

导出目录只包含 TsFile,导入后得到只读数据集;这一过程不是恢复完整 SQLite 数据库 或可写 hybrid 状态的备份协议。

11.4 验收场景

  • 导出目录只含标准 .tsfile;热数据不被导出,watermark 不改变,无冷数据返回 0;
  • 目录内多文件、多表能被完整发现,同表跨文件汇成一个只读表;
  • 不带本扩展私有 Properties 的普通 TsFile 可以查询,缺失精度标为 unknown;
  • 多文件重叠时间和重复键按 UNION ALL 保留;NULL TAG 与空字符串不混淆;
  • schema 不兼容、精度冲突、表名冲突及损坏文件使批量导入失败且不留下部分注册;
  • 对导入表执行 INSERT、UPDATE、DELETE、seal 均被拒绝,源文件字节保持不变;
  • 文件移走、替换和新增分别被诊断为缺失、变化和未登记;verify 不修改任何文件;
  • 删除只读逻辑表、导入失败及进程重启清理都不会删除外部文件;
  • 对导出复制、文件同步、目录发布和导入登记提交注入故障,验证源数据不变及 目标没有部分可见的发布结果。

12. 常见问题

加载时报 not authorized 或扩展加载被禁用

确认 SQLite 构建允许 loadable extension,并在应用连接上调用 sqlite3_enable_load_extension()。CLI 使用 .load 即可。

加载时报找不到 libtsfile

把当前构建对应的 libtsfile 与 tsfile_sqlite 放到同一目录,避免混用不同 版本的库。必要时检查平台动态链接器的搜索路径。

创建表时报目录错误

directory 必须是绝对路径,父目录必须可创建或可写。连接已有数据库时,该 目录必须仍然存在。

seal 后无法更新某些行

这是 watermark 的预期行为。任何 time < watermark 的数据已经进入不可变 冷区。

查询没有固定顺序

虚拟表会合并多个来源,但不承诺自然顺序。需要顺序时使用显式 ORDER BY。

13. MVP 限制

  • seal 只能显式、同步执行;
  • 冷数据不可更新或删除;
  • 不支持后台自动封存、compaction 和冷段删除;
  • 不支持原地 schema 演进;
  • 查询游标当前会在内存中汇集冷热结果;
  • 不跨冷热来源下推排序和 LIMIT/OFFSET;
  • Windows 尚未支持;
  • 这仍是实验性扩展,接口和内部格式可能继续演进。