扩展版本与兼容性¶
每个 DATUS 载荷都可以用 v 声明它按哪个版本编写,并在 requires 里列出旧引擎
不能丢弃的键,两者都在信封一节说明。任何引擎都能
报告自己实现的版本:dosi info、GET /v1/capabilities,或
dosi_engine.DATUS_EXT_VERSION。这一页讲版本号背后的规则,并记录每个版本加了什么。
版本策略¶
本引擎读取的每个键都登记了引入它的版本,以及消费方忽略它的代价。
这份登记表内置在引擎里,是唯一事实来源:
版本闸门、基础模式的告警文案、dosi info 和 GET /v1/capabilities
都在运行时读取它,所以它们谁也不可能与之漂移。下面的变更记录是照着这份登记表手写的。
忽略影响级别,按危险程度递增排列 —— 注意大声失败排在比悄悄返回不同数字更安全的位置:
| 影响 | 含义 |
|---|---|
inert |
引擎从不消费的展示类元数据。结果完全相同。 |
degraded |
不会出错误数字 —— 请求会大声失败(一个结构化错误,或某个查询名不再能解析)。 |
documented |
数字会变,但变得可预期、方向有文档可依(LEFT 对 INNER,NULL 对 0)。 |
silent |
数字是错的,而调用方无从察觉。必须列进 requires。 |
已发布的键里有三个是 silent:1.4 加入的 measure(D-MEASURE)、1.6 加入的
time(D-FORMAT)和 1.8 加入的 cardinality(D-CONFORM),见变更记录。
其余每个键都是 degraded 或 documented,所以对这三个都不使用的模型来说,基础模式
依然自洽。用了其中之一的模型必须把它列进
requires,正是这一条阻止了没有实现该键的 datus 模式引擎(旧版 Dosi、
其他厂商读这个信封的实现)悄悄算出另一个数字:它会在版本闸门处以
datus_ext_key_required 失败。基础模式是另一回事 —— 它根本不读载荷,requires
也不读,只会告警 ignored_vendor_extension;带 silent 键的模型不应在基础模式下运行。
什么时候提升版本号:
- MINOR —— 纯增量:一个新键,或者某个已有键的一个新可接受取值
(老载荷不可能用过它)。老载荷原样继续工作;新载荷跑在老引擎上会得到
datus_ext_version_ahead,只损失那个新键。 一个影响为silent的新键仍然只是小版本提升, 但生产方必须把它列进requires。 - MAJOR —— 只有当某个已有键改变了含义、被重命名或被撤销时。
此后每个声明了更旧
v的载荷都会按改变了的键被逐一拒绝, 并给出更新 YAML 的指引。 - 两者都不 —— 不改变任何键契约的引擎内部工作。 一个背后没有任何键的版本会让登记表的不变式变得空洞。
变更记录¶
每个已发布的小版本,以及它新增的键。这张表的机器可读形式是
capabilities().history —— dosi info --format json 或 GET /v1/capabilities。
| 版本 | 日期 | 新增的键 | 忽略时的影响 |
|---|---|---|---|
| 1.0 | 2026-07-11 | join_type (D-JOIN)、fill_nulls_with (D-FILL) |
documented、documented |
| 1.1 | 2026-08-02 | time_dimension (D-TIME)、time_granularity (D-GRAIN)、dataset (D-DATASET) |
degraded、documented、degraded |
| 1.3 | 2026-08-09 | (无 —— window 的族内扩展) |
(继承 window 的 documented) |
| 1.4 | 2026-08-12 | derive(D-DERIVE M1)、measure(D-MEASURE) |
degraded、silent |
| 1.5 | 2026-08-26 | params(D-PARAM M1) |
degraded |
| 1.6 | 2026-09-02 | time(D-FORMAT) |
silent |
| 1.7 | 2026-09-11 | is_dimension(D-DIM) |
documented |
| 1.8 | 2026-09-16 | cardinality(D-CONFORM) |
silent |
| 1.9 | 2026-09-16 | (无 —— window 的族内增长) |
(沿用 window 的 documented) |
| 1.10 | 2026-09-23 | lod(D-LOD;以及 derive 的 lod 家族) |
degraded |
| 1.11 | 2026-10-07 | (无 —— lod 的族内扩展) |
(沿用 lod 的 degraded) |
| 1.12 | 2026-10-25 | concept_params(D-PARAM,在 mapping 文档上);params 扩展到 field 载体 |
degraded |
- 1.0(2026-07-11):初版 ——
D-JOIN(关系的join_type, 已实现:据此生成 INNER/LEFT 连接)、D-FILL(指标的fill_nulls_with, 2026-07-12 已实现:对指标输出做 COALESCE; 覆盖 S-FAN-6 的计数默认值,对任何种类的指标都适用)。 - 1.1(2026-08-02):时间与归属 ——
D-TIME(数据集/指标的time_dimension,已实现:数据集的主时间维度与指标自己的时间维度, 保留的metric_time查询名, 按分支的时间范围回退)、D-GRAIN(字段的time_granularity, 已实现:更细粒度的请求 →grain_too_fine)、D-DATASET(指标的dataset,已实现:COUNT(*)的归属)。 基础模式的ignored_vendor_extension告警现在覆盖全部五种载体对象 (此前只有 Relationship 和 Metric)。 - 1.1(2026-08-04,未提升版本号):信封版本闸门本身 ——
v现在会被读取并强制执行(见信封),引入了requires, 每个键都带上了声明的忽略影响,整份登记表通过dosi info、GET /v1/capabilities和dosi_engine.DATUS_EXT对外发布。 没有新增或改变任何键,所以版本保持在 1.1。不带v的载荷 —— 也就是 Datus Agent 今天输出的那种 —— 不受影响。 - 1.2(2026-08-06):窗口指标 ——
D-WINDOW(指标的window, 已实现:即--explain里的WindowStage步骤; pop/rolling/cumulative 语法糖 + 通用的 offset/frame 形态, 带扫描扩展 + 窗口后裁剪的累计重置;忽略影响为documented—— 指标退回到它朴素的基础聚合;规范见window-extension.md)。 最初只在 DuckDB 上执行;全方言启用(按方言的偏移移位下降, 并在真实数仓上验证)作为后续落地。 族内新增(同一小版本,在早于它们的引擎上会 fail-closed): 放宽了混合族/混合移位/混合的带起始重置, 以及有限窗框上的require_full_window(2026-08-06)—— 见window-extension.md#restrictions。 - 1.3(2026-08-09):窗口族与函数 —— 在族内扩展
window键, 没有新的登记表行(capabilities().history仍然停在 1.2; 这个版本号标示的是载荷方言)。新增内容:rank族 (在指标值之上的 row_number / rank / dense_rank / ntile / percent_rank / cume_dist)、value族(first_value / last_value / nth_value)、 偏移的direction: "forward"(LEAD —— 下一期参照)、窗框的order/partition/units/second修饰符 (按值排序的窗框和 RANGE 窗框、分区模式、双参数输入), 以及登记表层级 W2(stddev_pop/samp、var_pop/samp) - W3(
covar_pop/samp、corr)。每一项新增在 1.2 引擎上都会 fail-closed: 未知的顶层族键会落进"恰好一个族"的错误,未知的族内键会被拒绝, 未知的函数名会在枚举解析时失败 —— 而且声明"v": "1.3"本身还会触发版本闸门。规范见window-extension.md(各族章节 + 函数登记表)。 - 1.4(2026-08-12):派生指标与度量命名 ——
D-DERIVEM1 (derive键,已实现:filter+compose两族、带成环检测的 指标引用解析、表达式等价性闸门、编译期一致维度集, 以及在每个宿主面上暴露的 derive 元数据;reagg/ 跨数据集的filter谓词 /window.base属保留形态,会以not_implemented拒绝并点名各自的里程碑 —— 注意window.base此前会落进"未知键静默忽略"那一档,现在改为 fail closed), 以及D-MEASURE(measure键,已实现:显式度量命名, 按签名去重和冲突检查保持不变 —— 这是对非 ASCII stem 冲突的修复, 也是第一个silent影响级别的键:请把它列进requires)。 M2(2026-08-19,族内 —— 没有新键,也不提升版本号):window.base从保留形态转为可用(窗口作用在叶子指标/filter 指标之上),compose 接受 窗口成员(阶段层下降:算术作用在窗口输出之上)和 filter 成员(展平层)。 用到这些形态的 1.4 载荷在 M2 引擎上能编译,在 M1 引擎上则 fail closed (not_implemented或直接拒绝)—— 绝不会被静默误读。 嵌套 compose(族内 —— 没有新键,也不提升版本号):compose 的成员可以 是另一个 compose,在编译期内联展开。老引擎遇到这个形态还是报原来那条invalid_datus_extension,同样是 fail closed。指标仍然留在展平层,derive_members变成传递展开后的叶子列表、系数逐层折叠; 树里任何位置有窗口,这个组合依旧是not_implemented。 - 1.6(2026-09-02):时间存储格式 ——
D-FORMAT(字段的time块, 已实现:分组侧按方言解析该列,过滤侧在生成 SQL 前就把边界折算成存储格式, 粒度从布局推断)。time.granularity与扁平的time_granularity成为地位相同的两种写法 —— 不废弃任何一种, 但两者不允许矛盾。这是第二个silent影响级别的键:请把它列进requires, 这样缺少它的 datus 模式引擎会在版本闸门处失败(基础模式不读requires,见信封)。 丢弃它的消费者不会响亮地失败;它会假定这是一个原生日期列, 而对按月或按年存储的列,结果是一个空结果集且没有任何诊断。 配置方法与限制见 D-FORMAT。范围:字符串与整型布局, 外加原生时间类型;epoch_seconds/epoch_millis暂缓, 因为from_unixtime会读会话时区,那套语义值得单独一轮。
现有键扩展(2026-09-08,版本仍为 1.6):time.format 支持连续、有序、定宽的时间部分及任意固定文本。
过滤仍将日期边界转换为存储编码常量;分组按位置提取时间部分,组成标准日期时间后
交给各数仓解析。原有格式保持兼容,旧版 Datus 引擎会拒绝新增格式,不会静默误解。
- 1.7(2026-09-11):字段角色 —— D-DIM(字段的 is_dimension,
已实现:由 list_dimensions 与归因的候选清单读取)。
模型终于能说清一列是度量、标识、自由文本还是 PII,从而不再被当作分组维度推荐。
它只是推荐信号:显式写出的 group_by 照常生效,编译出的 SQL 一个字节都不变,
因此影响级别是 documented,也不需要写进 requires。不写这个键时,引擎只依据
模型结构推断 —— 键、关系列、被数值聚合的列 —— 绝不探测数仓。见 D-DIM。
- 1.8(2026-09-16):事实表之间的一致性关系 —— D-CONFORM(Relationship 上的
cardinality 键,已实现:连接图里的一致性类、
group-by/过滤/时间范围列与 metric_time 逐分支解析到各事实表自己的拼写、
按输出名合并)。many_to_many 边永远不 join;只有配对列是共享的。
这是第三个 silent 影响级别的键:请把它列进 requires —— 没有它,这条边就是
many→one join,对重复敏感的聚合会被扇出。见 D-CONFORM。
- 1.9(2026-09-16):一张排名记分卡 —— 与 1.3 一样是 window 键的族内增长,
因此没有新的登记表行。同一个形态带出四项新增。
frame.scope: "partition" 把帧声明成整个分区,
而不是相对当前行的偏移,编译出 <函数>(…) OVER (PARTITION BY …):
既没有 ORDER BY,也没有帧子句。它就是排名旁边的第二个窗口函数 ——
「我是在多少行里排这个名次」—— 打分卡需要它,而现有的键组合不出来:
count 配 preceding: "unbounded" 是累计行数,units: "range"
也只到当前行所在的并列组为止。scope 与 preceding、reset、
require_full_window、units、order 互斥,后面这些只描述有序帧,
写了就报错而不是忽略。
window.base 同时放宽:比率和 compose 现在都能当作窗口读取的序列,
于是「给某个率排名」「看某个率的环比」写得出来了。跨事实 compose 的序列要到分支
合并之后才成为一个值,所以窗口阶段落在合并之上 —— 各分支分别聚合,它们的 CTE
与阶段的基座并排。只有事实表的日期经 D-CONFORM 配对时,这类基座
才有共享的时间轴;否则需要时间轴的那些族会以一致性为由拒绝。
在 1.8 引擎上双重 fail closed:frame 和 order 里的新键会被当作未知键拒绝,
而载荷里的 "v": "1.9" 也会触发版本闸。
见 window-extension.md。
嵌套窗口与 NULL 位置把这个形态补齐。order.nulls: "first" | "last"
钉住窗口排序里 NULL 落在哪一端 —— 不写的话各数仓按自己的默认来,而它们并不一致
(DuckDB 与 PostgreSQL 升序排最后,MySQL 一系与 StarRocks 排最前),
基座是比率之后这就从理论风险变成了常态。生成器只在与目标方言默认不一致时才写出该子句;
方言根本没有这个子句时(MySQL、TiDB),位置声明降为一个打头的 CASE WHEN … IS NULL
排序键 —— 所以渲染随方言变、含义不变。而 window.base 比「比率」更进一步:它接受「建在另一个窗口输出之上的
compose」,于是第二个窗口能读第一个窗口的输出。每一层编译成多一个 SQL 阶段;
层数上限为 3,第 2 层起必须显式声明 partition。要紧的护栏是跨层的而不是层内的:
输出的 trim 始终留在最外层之后,而任何一层的分区全局指标都不能与任何一层会扩大
扫描范围的指标共处一条查询。另外还放宽了:阶段层的 compose 可以有 compose
成员(它保持为成员而不被内联 —— 在那一层每个成员都是一条独立序列)。
- 1.10(2026-09-23):细节层次 —— D-LOD,一套语法、两个载体:Field 上的
lod 键,以及 Metric 上现有 derive 键的 lod 家族(已实现)。
写在字段上,fixed(base, dims…) 让一个「按实体聚合」的值可以被普通查询分组、
过滤、再聚合(即 --explain 里的 LodRollup 步骤;D-FORMAT 编码的汇总键
保留存储原值,只在外层解析一次)。视图需要的超出汇总本身时 —— 汇总键之外的维度、
读取该值的行级字段、多个键族 —— 走广播形态(LodBroadcast):明细行与每个键族
各一张派生表连接;对钉住值做聚合时,每个键在每组里只算一次。不写键就是整表
范围的 FIXED,广播到每一行。
写在指标上,include 按查询自身的 group-by 加上声明的维度分组内层,再用声明的
外层聚合收回来;粒度与 D-FORMAT 解析都在内层完成。exclude 把同一张事实表更粗
的一次聚合挂到视图的每一行上 —— 按共有的维度 LEFT JOIN,没有共有维度就 CROSS
JOIN —— 是重新计算而不是开窗,所以 COUNT(DISTINCT) 基座也是对的。fixed
是字段那份声明挂在指标上,好让它挂到视图上:在维度筛选之前算出的整表或按键
总计,也就是 Tableau 的 {SUM([Sales])} 作分母时的含义,exclude() 表达不了
(见 LOD 指标)。一致性配对的两端若是两张事实表各自的 fixed
字段,就把两者对齐成同一根轴:每张事实表各自汇总,分支再合并,两张事实表从不
连接。
筛选位置沿用 Tableau 的运算次序。where 是维度筛选 —— 只点名汇总键时作用在
固定汇总的输出上,否则作用在明细行上,从不进入汇总内部;time_range 是上下文
筛选,约束每一个汇总。只读 D-LOD 值和常量的行级字段按键各聚合一次,
也可以在该键粒度上筛选再聚合。因此,像 customer_margin >= 5000 AND customer_orders >= 6
这样的计算分类会先筛选 fixed 值,再聚合。条件读到另一组 fixed 键时,
会在该组汇总值仍可用的位置、网格归并之前求值。
基础模式丢弃 D-LOD 字段(aggregate_field_dropped),对 D-LOD 指标则 fail
closed:它的 expression 就是声明所描述的嵌套聚合(nested_aggregate),
而不是只算其中一层。见理解 LOD。
- 1.11(2026-10-07):跨数据集的细节层次 —— lod 键的族内扩展,没有新的登记表行。
字段的 fixed base 可以聚合字段所在数据集以外的数据集,前提是每个键都经声明过的关系对应到那边的一列:
两表之间任一方向的多对一边、base 到宿主表的多对一路径,或者 D-CONFORM 配对
(一个有自己扫描的汇总)。
汇总按普通等值左关联到宿主表的行上,没有匹配时计数读 0,所以 > 0、= 0、>= n
分别是半连接、反连接和阈值。上下文经 base 自己的关联边或配对进入汇总,那边读不到的合取项不进入
(跨数据集)。1.10 的引擎会拒绝这种 base(报它聚合了另一个数据集),不会算错。
- 1.12(2026-10-25):字段与派生子类型上的参数。params 扩展到 field 载体 ——
字段表达式里的 :name 洞,即 field.literal 槽位,绑定时把字段在整个查询里打补丁,
并在 bindings 下回显(字段参数);concept_params 落在 Ossie
mapping 文档上,声明派生子类型 derived_by
规则里的占位符(子类型阈值)。同期语义契约新增 S-ATTR-3′:
CASE WHEN <guard> THEN <arg> END 形态的聚合,其 guard 可以读 arg 所在数据集多对一可达的
一个数据集 —— 事实表指标的 D-DERIVE filter 因此可以写成
where: "Customer is PremierCustomer"。旧引擎 fail closed:concept_params 是未知键,
字段里的 :name 不是列,"v": "1.12" 触发版本门。
尚未实现(路线图)¶
排队中的这些 RFC 全部是增量的,所以每一个都以一次小版本提升落地, 带上自己的登记表行。顺序可能变化;每一条都独占一个小版本。
| 小版本 | RFC | 键 | 载体 | 影响 | 需要 requires? |
|---|---|---|---|---|---|
| ≥1.12 | rfc-semi-additive-metrics §4 |
semi_additive |
Metric | silent —— 期末余额会被跨快照加总 |
是 |
| ≥1.12 | rfc-minimal-declarations §3 D-DISTINCT-STATE |
distinct_state |
Field / Dataset | silent —— 一个物化的 bitmap 列会被当作普通列聚合 |
是 |
| ≥1.7 | rfc-derived-metrics M3-M4 |
derive 的 reagg 与一般的跨数据集 filter(window.base 与 compose 的窗口/filter 成员已在 1.4/M2 以族内方式发布;单个维度守卫已在 1.12 作为 S-ATTR-3′ 发布) |
Metric | degraded/错误 —— 保留形态今天就以 not_implemented 拒绝,绝不静默计算 |
否 |
| — | window-extension.md 的 rank/share/streak 族、W2+ 函数 |
将来的 window 形态(族内新增,在旧引擎上 fail-closed) |
Metric | degraded |
否 |
已经发布的 silent 键有三个:1.4 的 measure(D-MEASURE)、1.6 的
time(D-FORMAT)和 1.8 的 cardinality(D-CONFORM)。如果你在一个会输出
silent 影响级别键的 Datus Agent 上编写模型,请留意它产生的 requires 列表:
正是它阻止了更旧的引擎悄悄返回错误数字,而这之所以行得通,
是因为 requires 本身在 1.1 就已发布 —— 早于第一个需要它的键。