tsfile_sqlite 是一个实验性的 SQLite loadable extension。它提供 tsfile_hybrid 虚拟表,让一张逻辑表同时使用两种物理存储:
它适合以追加为主、近期数据偶尔需要修正、历史数据可以冻结的时序场景。 它不是 SQLite 通用表的替代存储引擎,也不会自动把已有 SQLite 表转换为 TsFile。
libtsfile;当前 MVP 不支持 Windows。
在仓库根目录执行:
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 子目录:
cpp/build/sqlite/lib/tsfile_sqlite.socpp/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
.load /absolute/path/to/tsfile_sqlite
SQLite CLI 通常会根据平台自动补全 .so 或 .dylib 后缀。也可以传入完整 文件名。
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 表之前加载扩展。生产环境建议加载 完成后立即关闭动态扩展加载能力。
以下按目标功能设计定义建表语法;示例表达待实现接口,不代表当前原型已支持。 列定义采用 列名 类型 [类别],省略类别时默认为 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;FIELD,FIELD 值可以为 NULL;Temperature 和 temperature 视为重名;_tsfile_command、_tsfile_cutoff 控制列。NULL TAG 的目标语义:
NULL、空字符串 '' 和字符串 'null' 是三个不同值,封存和查询必须保留区别;(device=NULL, region='cn-east') 记录冲突;IS NULL,普通 WHERE 表达式继续遵循 SQLite 的 NULL 语义, 不把 = 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)
| TsFile 类型 | SQLite 表现 | 写入要求 |
|---|---|---|
BOOLEAN | INTEGER | 必须传 SQLite INTEGER;0 为假,非 0 为真 |
INT32 | INTEGER | 必须在 int32 范围内 |
INT64 | INTEGER | SQLite int64 |
FLOAT | REAL | INTEGER 或 REAL |
DOUBLE | REAL | INTEGER 或 REAL |
TEXT | TEXT | SQLite TEXT |
STRING | TEXT | SQLite TEXT |
BLOB | BLOB | SQLite BLOB,保留长度和二进制零字节 |
DATE | INTEGER | 必须在 int32 范围内 |
TIMESTAMP | INTEGER | SQLite int64 |
扩展不会换算时间戳。timestamp_precision 仅声明整数时间戳的单位,并写入 非空 TsFile 段的 tsfile_sqlite.timestamp_precision property。
schema、目录和时间精度会记录在配置 shadow table 中。数据库重新打开时, 扩展会验证这些信息是否与 CREATE VIRTUAL TABLE 中保存的参数一致。
当前版本不支持修改 schema、目录或时间精度。需要变更时,应创建一张新的 逻辑表并迁移数据。
普通 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;
无论数据位于 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 结果正确。
使用两个隐藏列发送 seal 命令:
INSERT INTO sensor(_tsfile_command, _tsfile_cutoff) VALUES ('seal', 1700086400000);
cutoff 是不包含的上界。上述操作封存:
旧 watermark <= time < 1700086400000
其中 time == 1700086400000 的行仍在热区,可以继续修改。
一次非空 seal 会:
.tsfile;如果区间内没有数据,不生成 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 的行继续留在热区;也可以由调用方显式控制事务:
BEGIN IMMEDIATE; SELECT tsfile_seal('main.sensor', 1700086400000); COMMIT;
该 UDF 属于有副作用的管理操作,规定以独立顶层 SELECT 调用,不支持放入 逐行查询、视图、触发器或其他 schema 表达式。注册时不标记为 deterministic, 并限制间接调用。业务接口不再要求向隐藏控制列插入数据。
如果事务回滚,manifest、watermark 和热数据删除都会回滚,扩展也会删除本次 事务产生的临时文件或已经改名的段文件。
watermark 把逻辑时间轴分成两部分:
time < watermark 冷区:不可修改 time >= watermark 热区:允许 INSERT/UPDATE/DELETE
以下操作会返回约束错误:
time < watermark 的行;(所有 TAG, TIME) 唯一键。当前版本没有 correction 或 tombstone。如果业务必须修正历史数据,需要重建 逻辑表或在业务层保留单独的修正数据。
每张名为 sensor 的 hybrid 表拥有三个 SQLite shadow table:
| 表 | 内容 |
|---|---|
sensor_data | 可变热数据 |
sensor_segments | TsFile 路径、cutoff 和行数 manifest |
sensor_config | watermark、精度、目录和 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 是内部 实现标识,不应作为跨查询或跨版本稳定的业务主键。
每张逻辑表必须使用独占目录。扩展会在 seal 前清理目录中未被 manifest 引用的 .tmp 和 .tsfile 文件,用来恢复进程崩溃后遗留的孤儿文件。因此不要把手工 创建的 TsFile、其他表的段文件或任何同后缀文件放进该目录。
执行:
DROP TABLE sensor;
会删除虚拟表及其三个 shadow table,但不会删除已经导出的 TsFile。删除或归档 这些文件需要由运维流程显式完成。
本节定义面向标准 TsFile 的交换和查询接口。导出产物只有 .tsfile 文件,不导出 SQLite 数据库、热表、WAL、shadow 表或独立的 JSON 清单,也不承诺恢复源表的可写状态。 导入接口及只读语义在 11.2 节定义。
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
第一版采用保守的一致性方式:取得源 SQLite 数据库的写保留锁,在同一事务视图内 固定该表的文件登记集合,复制或重写完成后释放锁。期间阻塞该数据库的其他写入。 对外部只读文件也要检查读取错误和文件变化;外部修改不属于 SQLite 锁的保护范围。
先在目标旁的专用暂存目录生成文件,校验 footer、schema 和已知精度,完成文件同步, 再原子发布整个目录并同步父目录。失败不发布完整目标,源文件不变。 仅允许独立管理调用,不接受用户已有的显式事务;成功返回时输出目录已发布。
SELECT tsfile_import('/data/archive');
tsfile_import(directory [, target_schema]) 默认把发现的表注册到 main;可显式 指定一个已经存在的 SQLite schema。成功返回新注册的逻辑表数量。
SELECT tsfile_import('/data/archive', 'archive');
这里的导入指登记外部 TsFile 并建立查询入口:默认直接引用原文件,不复制、移动、 重写文件,也不把行装入 SQLite 热表。源目录必须是可持续访问的绝对路径,文件需 由调用方保持不可变。SQLite 中只保存查询所需的文件登记和 schema 元数据。
扫描与分组规则:
.tsfile 文件,不递归子目录,不跟随符号链接; 非 TsFile 文件忽略,扩展名匹配但无法读取的文件使本次导入失败并报告路径。UNION ALL 语义读取,重叠时间和重复逻辑键保留,不自动 覆盖、去重或取最新值。只读导入不对外部数据强加可写 hybrid 表的唯一键约束。用户通过普通 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。 本次扫描后新加入目录的文件不会自动进入已注册表,第一版不提供后台监控或刷新; 如需重新登记,可删除相关只读逻辑表后再导入,删除逻辑表不会删除外部文件。
SELECT * FROM tsfile_verify('main.sensor');
tsfile_verify(table_name) 是只读表值接口,返回 segment_id, path, status, detail。状态包括 OK、MISSING、CORRUPT、 MISMATCH 和 UNREGISTERED。导入时建立文件指纹,verify 对比文件内容、schema 和可用精度;普通文件不需要预先存储本扩展的校验属性。
.tsfile 后缀被孤儿清理删除。导出目录只包含 TsFile,导入后得到只读数据集;这一过程不是恢复完整 SQLite 数据库 或可写 hybrid 状态的备份协议。
.tsfile;热数据不被导出,watermark 不改变,无冷数据返回 0;not authorized 或扩展加载被禁用确认 SQLite 构建允许 loadable extension,并在应用连接上调用 sqlite3_enable_load_extension()。CLI 使用 .load 即可。
libtsfile把当前构建对应的 libtsfile 与 tsfile_sqlite 放到同一目录,避免混用不同 版本的库。必要时检查平台动态链接器的搜索路径。
directory 必须是绝对路径,父目录必须可创建或可写。连接已有数据库时,该 目录必须仍然存在。
这是 watermark 的预期行为。任何 time < watermark 的数据已经进入不可变 冷区。
虚拟表会合并多个来源,但不承诺自然顺序。需要顺序时使用显式 ORDER BY。
LIMIT/OFFSET;