这几年我一直在写一个 C++20 实现的 Flux 查询语言实验项目:cpp/pl/flux。它不是为了完整复刻 InfluxData 官方 Flux,也不是为了立刻做成生产级数据库,而是为了回答一个更工程化的问题:如果从零实现一个可运行、可调试、可测试的 Flux 子集,需要哪些模块,它们之间应该如何分层?
很多语言项目会停在 parser demo:能把源码解析成 AST,能打印一棵树,已经很有成就感。但查询语言更麻烦。它不仅要理解表达式,还要处理表流、group key、窗口、聚合、数据源、输出格式、IDE 体验和性能退化。只要其中一个边界没想清楚,后面就很容易把优化逻辑、运行时逻辑和标准库逻辑搅在一起。
目前这个项目已经不只是一个 parser。它包含 scanner、parser、AST dump、表达式解释器、运行时值模型、标准库 package、表流执行、SQLite/MySQL connector、查询计划、Page-based physical executor、CLI、REPL、LSP 和 conformance examples。换句话说,它已经有了一个小型语言运行时和单机查询引擎的骨架。
这一篇是整个系列的入口。我会先讲项目目标和能力边界,再讲一条 Flux 查询从源码到输出会经过哪些层,最后给出代码阅读和运行方式。后面的文章会沿着这些边界逐层展开。
Flux 很适合拿来做查询引擎实验,因为它同时具备三种特征。
第一,它是一门表达式语言。它有字面量、对象、数组、函数、闭包、条件表达式、正则、字符串插值和命名参数。实现它时,必须认真处理 scanner、parser、AST、runtime value、environment 和 function call。
第二,它是一门 pipeline 查询语言。|> 不是普通装饰语法;它决定了用户如何把数据源、过滤、投影、聚合和输出串起来。实现 pipe 后,运行时必须决定:一个表算子是立即执行,还是追加到 lazy logical plan 等待 optimizer 处理?
第三,它围绕 table stream 建模。Flux 查询不是简单返回一个数组,而是返回一组 logical table,每张表有 group key、列、行和结果名。这让它天然适合探索查询引擎里的 table pipeline、window、aggregate、join 和 connector pushdown。
一段很小的 Flux 查询就能覆盖这些问题:
import "array"
array.from(rows: [
{_time: 2024-01-01T00:00:00Z, host: "edge-1", region: "east", usage: 91},
{_time: 2024-01-01T00:01:00Z, host: "edge-2", region: "west", usage: 42},
])
|> filter(fn: (r) => r.usage > 80)
|> keep(columns: ["host", "usage"])
|> yield(name: "hot_hosts")
为了让它真正跑起来,项目至少需要完成这些事情: