Datus 对 OSI 的扩展(厂商规范)¶
- 状态:草案,Datus 已在用(Dosi 与 Datus 的模型生成 Agent)。
- 规范版本:
datus-ext/1(主版本系列) - 当前版本:1.1 —— 每个小版本加了什么、以及版本号如何决定,
见 §2.1。任何引擎都能报告自己实现的版本:
dosi info、GET /v1/capabilities,或dosi_engine.DATUS_EXT_VERSION。 - 范围:OSI 核心
0.2.0.dev0还无法表达的那些语义, 统统装在 OSI 官方认可的custom_extensions字段内部, 这样文档仍然是完全合法的 OSI,而每个非 Datus 的消费方都会忽略它们。
想要带前后对比示例的任务式讲解,见 Datus 扩展指南。这一页是规范性契约。
除了模型语义之外,Dosi 的连接配置也讲 Datus: 配置沿用 agent.yml 的
datasources:词汇表,一份完整的agent.yml可以直接作为--connections传入 —— 见 connectors.md。
这是一个私有的厂商扩展,不必经 OSI 工作组接纳即可使用。
当某个扩展所泛化的空缺本该属于核心规范时(时间粒度、累计窗口),
它的长期归宿是
rfc-time-semantics.md
里的 OSI RFC;本文档是 Datus 今天据以发布的过渡载体。
1. 为什么这对 OSI 是安全的¶
OSI 的 schema 很严格(到处都是 additionalProperties: false),
所以我们不能直接加 relationship.join_type 这样的裸字段。
但规范在每个主要对象上都提供了一等公民级别的逃生舱:
custom_extensions 存在于 SemanticModel、Dataset、Field、Relationship
和 Metric 之上(见 crates/dosi-model/src/spec.rs)。上游的 ossie 校验器
接受它,所以一份携带 Datus 扩展的文档原样通过 validate-osi 这道关。
不认识 vendor_name: DATUS 的消费方直接跳过,
模型的含义仍精确等于纯 OSI 所说的含义。Datus 扩展只在 OSI 未作定义之处
细化引擎行为;它们绝不改变一个指标或维度是什么。
2. 信封¶
一个 Datus 扩展就是一条 custom_extensions 条目:
vendor_name:规范写法是大写常量"DATUS"(与 OSI 规范的大写枚举风格一致, 比如ANSI_SQL)。匹配时不区分大小写;历史上的小写datus继续可用。data:一个 JSON 字符串(按 OSI 规范,data是字符串而不是对象), 解码后是单个 JSON 对象 —— 即载荷。每个对象上最多一条DATUS条目。 有两个信封键是保留的:v和requires。
消费规则(Dosi):
- 不存在 → 采用下文写明的 OSI 默认行为。扩展纯粹是增量的, 一个没有任何扩展的模型,行为与今天完全一致。
- 未知的载荷键会被忽略,而不是被拒绝:这样更新的 Datus Agent
可以输出老引擎不读的键,而不至于把它弄坏(前向兼容)。
例外是下面的
requires。 - 未知的
vendor_name(除DATUS之外的一切,比较时不区分大小写)会被忽略。 - 格式不对的
DATUS载荷(不是 JSON,或某个已知键的类型不对)是一个 结构化错误(invalid_datus_extension),而不是悄悄取默认值: 一份由 Datus 编写的文档要按 Datus 的契约来要求。编译仍会带着默认值继续, 这样一趟就能报出模型里的所有问题。
v —— 声明的版本¶
v 的形式是 MAJOR.MINOR。字符串形式是规范写法("v": "1.1");
为方便起见也接受 JSON 数字("v": 1.1,以及裸整数 "v": 1 表示 1.0)。
JSON 数字只能表达 0–9 的小版本号。
1.10和1.1是同一个f64, 所以数字无法表达小版本 10;带两位及以上小数的数字会被直接拒绝, 并提示你改用字符串形式。一旦当前小版本到达 10,请一律把"v"写成字符串。
v 是可选的,省略它永远是安全的 —— 一个未声明版本的载荷,
行为与版本闸门存在之前完全一致,也不会产生任何诊断信息。
版本是按 custom_extensions 条目声明的,而不是按文档:
模型级的 v 不会成为它下面数据集、字段、关系或指标的默认值。
给定引擎自身的版本,标注版本能给你带来什么:
| 情形 | 引擎的反应 | 错误码 |
|---|---|---|
v 不存在或为 null |
静默 —— 行为与闸门出现之前一致 | — |
v 格式不对(坏字符串、JSON 类型不对、带 ≥2 位小数的数字) |
错误 | invalid_datus_extension |
v 低于 1.0,或主版本比引擎的新 |
错误 | unsupported_datus_ext_version |
| 小版本比引擎的新 | 告警并点名被丢弃的键;引擎认识的一切仍然生效 | datus_ext_version_ahead |
某个键比你声明的 v 更新 |
告警;但该键仍会被采纳 | datus_ext_key_newer_than_declared |
| 小版本比引擎的旧,且所用键都在其范围内 | 静默 | — |
未来的主版本是唯一一种一刀切的拒绝。主版本号提升意味着至少有一个键改变了含义, 而早于这次改变的引擎无从知道是哪一个,硬着头皮读那个载荷会产生悄无声息的错误数字。 小版本按构造就是增量的,所以它们最多只是告警。
requires —— 不允许被丢弃的键¶
requires 是一个键名数组,生产方借此声明这些键不得被悄悄忽略。
没有实现其中某个键的消费方会抛出 datus_ext_key_required 并点名该键,
而不是丢弃它。
它之所以存在,是因为消费方无法判断自己从没听说过的键有多危险。
对 join_type 这样的键,"未知键被忽略"是正确的默认:
忽略它只是退回到一个有文档的行为。但对 semi_additive 这样的键就是错误的默认:
忽略它意味着把期末余额加总起来,返回一个干脆就是错的数字。
生产方知道哪个是哪个,requires 就是它表达这一点的方式。
对每一个忽略影响为 silent 的键(见 §2.1)都要输出 requires,
其余一概不要。让老引擎降级运行,好过直接拒绝为它服务。
dosi info / GET /v1/capabilities 会报告每个键的影响级别。
2.1 版本策略¶
本引擎读取的每个键都登记了引入它的版本,以及消费方忽略它的代价。
这份登记表位于 crates/dosi-compiler/src/ext.rs,是唯一事实来源:
版本闸门、基础模式的告警文案、dosi info 和 GET /v1/capabilities
都在运行时读取它,所以它们谁也不可能与之漂移。§7 的变更记录是照着这份登记表手写的。
忽略影响级别,按危险程度递增排列 —— 注意大声失败排在比悄悄返回不同数字更安全的位置:
| 影响 | 含义 |
|---|---|
inert |
引擎从不消费的展示类元数据。结果完全相同。 |
degraded |
不会出错误数字 —— 请求会大声失败(一个结构化错误,或某个查询名不再能解析)。 |
documented |
数字会变,但变得可预期、方向有文档可依(LEFT 对 INNER,NULL 对 0)。 |
silent |
数字是错的,而调用方无从察觉。必须列进 requires。 |
今天发布的键里没有任何一个是 silent,
这恰恰是基础模式作为一个选项能自洽的原因:目前忽略 Datus 扩展总是安全的。
什么时候提升版本号:
- MINOR —— 纯增量:一个新键,或者某个已有键的一个新可接受取值
(老载荷不可能用过它)。老载荷原样继续工作;新载荷跑在老引擎上会得到
datus_ext_version_ahead,只损失那个新键。 一个影响为silent的新键仍然只是小版本提升, 但生产方必须把它列进requires。 - MAJOR —— 只有当某个已有键改变了含义、被重命名或被撤销时。
此后每个声明了更旧
v的载荷都会按改变了的键被逐一拒绝, 并给出更新 YAML 的指引。 - 两者都不 —— 不改变任何键契约的引擎内部工作。 一个背后没有任何键的版本会让登记表的不变式变得空洞。
3. 扩展目录¶
D-JOIN —— 关系的连接类型¶
位置:一个 Relationship 的 custom_extensions。
载荷键:join_type,枚举 "left" | "inner"。
默认(不存在时):"left"。
控制一个扇出分支在沿这个关系(多→一)把"一"端 JOIN 进来、 以到达某个维度或某个不在基上的度量时,行为如何:
"left"(默认):保留多端没有匹配的行 —— 孤儿事实行会存活下来,落进任何取自缺失那一侧的维度的 NULL 分组里。 对账/审计语义。"inner":丢弃未匹配的多端行。归因语义 —— 一个度量只在它所归因的实体确实存在时才被统计。
这让 INNER 还是 LEFT 成为关系的一个显式属性,
而不是生成器里写死的默认值(见 docs/semantics.md S-JOIN)。
relationships:
- name: sales_to_regions
from: sales
to: regions
from_columns: [region_id]
to_columns: [region_id]
custom_extensions:
- vendor_name: DATUS
data: '{"v": "1.0", "join_type": "inner"}'
D-FILL —— 合并时缺失分组的指标空值填充¶
位置:一个 Metric 的 custom_extensions。
载荷键:fill_nulls_with,一个 JSON 数字。
默认(不存在时):引擎默认 —— 一个纯 COUNT 指标填 0
(docs/semantics.md S-FAN-6);其余每个指标保持 NULL。
显式的 fill_nulls_with 优先于 S-FAN-6 的计数默认值,
并且对任何种类的指标都适用(SUM、比率、表达式),
在合并计划和单分支计划里都会把该指标的输出 COALESCE 成给定的数字。
在一次多分支合并之后,只出现在部分分支里的分组,
对那些在缺失分支中计算的指标会投影出 NULL。
fill_nulls_with: <n> 会把这个指标的值 COALESCE 成 <n>(通常是 0),
这样一个没有任何贡献行的分组就会报出一个具体数字而不是空白。
作用范围的保护(与 S-FAN-6 一致):填充作用于指标的输出,
绝不作用于嵌在比率/表达式里的某个度量 ——
一个计数作为分母时不能变成字面量 0 除数。对比率来说,请填充那个比率,
引擎仍会以空值安全的方式计算它。
metrics:
- name: sales_total
expression:
dialects:
- dialect: ANSI_SQL
expression: SUM(sales.amount)
custom_extensions:
- vendor_name: DATUS
data: '{"v": "1.0", "fill_nulls_with": 0}'
D-TIME —— 主(聚合)时间维度¶
位置:一个 Dataset 或一个 Metric 的 custom_extensions。
载荷键:time_dimension,一个字符串名称引用 —— 在数据集上,
是它自己某个 is_time 字段的名字;在指标上,是 "field" 或 "dataset.field"。
默认(不存在时):一个恰好只有一个 is_time 字段的数据集,
会把它作为隐含的主时间(这条推断不读取任何扩展,所以在基础模式下同样适用);
零个或多个 is_time 字段 → 没有主时间。
声明哪一列是一张表/一个指标的业务时间轴 —— 也就是当查询要求某个时间范围或粒度却没有点名列时,引擎会使用的那一列。 每个指标的解析顺序(S-TIME-5):
- 该指标自己的
time_dimension(优先级最高); - 否则取该指标所涉数据集中唯一的那个主时间(数据集显式的
time_dimension, 否则是单is_time推断)。
指标级的裸 "field" 先在该指标自己的数据集里解析
(像 etl_dt 这样的时间字段名会在多个数据集里重复出现),
只有当它们都没有这个字段时,才放宽到整个模型。
查询通过保留名 metric_time 来消费它
(--group-by metric_time:month、--time-dimension metric_time),
也通过时间范围的回退来消费:一个没有点名维度、group-by 里也没有时间项的范围
会去过滤每个指标的主时间 —— 按聚合分支分别进行,用该分支自己的那一列。
一个无法解析出主时间的指标会产生 no_primary_time_dimension;
共处一个分支却有不同主时间的指标会产生 metric_time_conflict。
被引用的字段必须存在且为 dimension.is_time: true,否则
invalid_datus_extension。
datasets:
- name: orders
custom_extensions:
- vendor_name: DATUS
data: '{"v": "1.1", "time_dimension": "order_date"}'
fields:
- name: order_date
dimension: { is_time: true }
- name: ship_date
dimension: { is_time: true }
metrics:
- name: shipped_revenue
expression:
dialects: [{ dialect: ANSI_SQL, expression: SUM(orders.amount) }]
custom_extensions:
- vendor_name: DATUS
data: '{"v": "1.1", "time_dimension": "ship_date"}'
D-GRAIN —— 原生时间粒度¶
位置:一个 Field 的 custom_extensions(该字段必须是
dimension.is_time: true)。
载荷键:time_granularity,枚举 "day" | "week" | "month" |
"quarter" | "year"。
默认(不存在时):未知 —— 可以请求任意的查询时粒度。
声明该列存储时所用的粒度(比如一份按月快照的 etl_dt)。
请求一个严格更细的粒度(在一个 month 列上请求 :day,
无论是通过显式 group-by 项还是通过 metric_time)会得到一个结构化的
grain_too_fine 错误,而不是悄无声息的错误数据。
截断到原生粒度或更粗的粒度则不受影响。
这是上游 RFC 里 dimension.time.granularity 的过渡载体。
fields:
- name: etl_dt
dimension: { is_time: true }
custom_extensions:
- vendor_name: DATUS
data: '{"v": "1.1", "time_granularity": "month"}'
D-DATASET —— 指标的归属数据集¶
位置:一个 Metric 的 custom_extensions。
载荷键:dataset,一个字符串,点名模型里的某个数据集。
默认(不存在时):归属只从指标 SQL 本身推导。
COUNT(*) 没有点名任何列,所以在一个多数据集模型里、
又没有别的聚合把某个数据集钉住时,OSI 让它的归属处于未定义状态,
引擎会报错(count_star_needs_dataset)。这个提示恰好解决那种歧义。
它绝不会覆盖从 SQL 推导出的归属 —— 一个其聚合点名了列的指标,
无论有没有这个提示都保留它们各自的数据集(扩展绝不重新定义 SQL 所说的东西)。
一个不存在的数据集名会是 invalid_datus_extension。
metrics:
- name: chat_message_count
expression:
dialects: [{ dialect: ANSI_SQL, expression: "COUNT(*)" }]
custom_extensions:
- vendor_name: DATUS
data: '{"v": "1.1", "dataset": "chat_record"}'
D-WINDOW —— 派生的窗口指标(同环比/滚动/累计)¶
位置:一个 Metric 的 custom_extensions。
载荷键:window,一个 JSON 对象 —— 完整 schema 见
window-extension.md。
默认(不存在时):该指标就是它那个朴素的基础聚合 —— 不做任何派生。
在指标自己的 expression(必须能推断为单个朴素聚合)之上声明一种窗口派生:
pop / rolling / cumulative 三种语法糖,
或者它们展开成的通用 offset / frame 形态。时间轴不在这里声明 ——
D-WINDOW 与 D-TIME 组合使用(指标的 time_dimension > 数据集的主时间;
缺失时间轴会在查询时以 no_primary_time_dimension 失败)。
偏移会下降成一个日历正确的移位键自连接,带自动扫描扩展 + 输出裁剪;
窗框会下降成 agg(v) OVER (… ROWS …),其中 reset 会加上一个 DATE_TRUNC
分区键,并有自己的扩展 + 窗口后裁剪。v1 的查询限制
(一个时间轴/一个族/一个偏移/一个带起始的重置/单分支)都是结构化的
not_implemented 错误;比所查询粒度更细的重置是 window_reset_too_fine。
D-FILL 绝不作用于派生输出(NULL 的含义是"没有可比的上一期")。
指标表达式里的裸窗口 SQL 仍然被拒绝(window_in_metric)——
这个扩展是唯一的窗口通路。
metrics:
- name: revenue_mom_growth
expression:
dialects: [{ dialect: ANSI_SQL, expression: "SUM(orders.amount)" }]
custom_extensions:
- vendor_name: DATUS
data: '{"v": 1, "window": {"type": "pop", "offset": "1 month"}}'
D-DERIVE —— 派生指标(filter / compose)¶
位置:一个 Metric 的 custom_extensions。
载荷键:derive,一个带 type 判别式的 JSON 对象。
默认(不存在时):该指标是自包含的,没有任何结构元数据。
规范:design/rfc-derived-metrics.md(M1 发布 filter + compose;
reagg 和 window.base 是保留形态,会以 not_implemented 拒绝,
并在提示里点名它们所属的里程碑)。
声明一个指标如何从同一模型的其它指标派生 —— 这是 OSI 表达式承载不了的结构信息。
该指标的 expression 仍然是一个合法且语义等价的 OSI 聚合(基础模式照样计算它,
得到相同的数字);引擎会在编译期强制这份等价性(derive_expression_mismatch),
所以请把这个表达式当作工具维护的产物,而不是可以手改的东西。
filter——{"type":"filter","base":<metric>,"where":<predicate>}:base的一个按维度过滤的变体。谓词会折进聚合的参数里 (CASE WHEN pred THEN arg END;COUNT类基础指标折成THEN 1), 而绝不是折成WHERE—— 不匹配的时间分桶依然存在(取值为 NULL), 正是这一点让该变体在 ROWS 窗框下也安全。M1 限制谓词只能作用于基础指标自己的数据集 (跨数据集谓词在 M4 之前是not_implemented),且base必须是一个非派生的 朴素单聚合。引擎会把子集关系(subset_of)记进指标元数据。compose——{"type":"compose","expr":<arithmetic>,"fill":<n>}: 在指标名之上做标量算术("revenue - total_refunds")。 标识符严格解析为指标名 —— 带限定符或未知的名字都是错误并附候选项, 绝不会悄悄当成列来读。聚合调用会被拒绝(组合绝不重新聚合)。fill在分支对齐之后、算术之前替换某个成员的 NULL (用于修正跨事实表时"缺失分桶毒化整个求和"的问题)。 引擎会识别线性组合(Σ cᵢ·metricᵢ),记录成员、系数和叶子度量的分解, 并在编译期固定下一致维度集(在唯一连接路径规则下,每个成员分支都能到达的维度) ——dosi list会暴露它,而在该集合之外分组的查询会得到unconformed_dimension, 并点名是哪个成员够不到这个维度。成员之间没有共同维度时, 编译期会给出compose_no_conformed_dimensions告警。 归因元数据只有在 SUM/COUNT 层级度量之上的线性组合中才标记为exact(一个被过滤的COUNT(DISTINCT)与它的补集会重叠 —— 永远不可能 exact)。
指标引用之间成环会是 metric_reference_cycle,并给出完整路径。
元数据在每个宿主上都会暴露(dosi list、REST、MCP、Python):
derive_family、derive_base、subset_of、derive_members、
leaf_measures、attribution、conformed_dimensions。
metrics:
- name: net_revenue
expression:
dialects: [{ dialect: ANSI_SQL, expression: "COALESCE(SUM(orders.amount), 0) - COALESCE(SUM(refunds.ref_amount), 0)" }]
custom_extensions:
- vendor_name: DATUS
data: '{"v": "1.4", "derive": {"type": "compose", "expr": "revenue - total_refunds", "fill": 0}}'
D-MEASURE —— 显式度量名¶
位置:一个 Metric 的 custom_extensions。
载荷键:measure,形如 {"name": <identifier>}。
默认(不存在时):由净化器生成的 stem(S-MEASURE-1)。
规范:design/rfc-minimal-declarations.md §2。
显式命名该指标唯一的那个合成度量。度量名原本是把聚合参数净化成纯 ASCII 得到的,
于是两个只在非 ASCII 内容上有差别的聚合(比如 '新品' / '老品' 这样的 CASE
字面量)会塌成同一个 stem,触发 measure_name_collision —— 模型根本编译不过。
显式名字是一个标签:去重仍然以聚合签名为键(一个没有声明的文本孪生体会共享这个
已声明的名字),这个名字也会进入同一套冲突检查(一个签名上有两个显式名字,
或一个名字落在两个签名上,同样会大声冲突),并且它只适用于单聚合指标。
由于度量名会出现在结果列和元数据里,忽略这个键等于悄悄改名 ——
所以请把它列进 requires,让更旧的引擎直接拒绝
(这是 requires 发布以来第一个 silent 影响级别的键)。
metrics:
- name: 新品收入
expression:
dialects: [{ dialect: ANSI_SQL, expression: "SUM(CASE WHEN orders.category = '新品' THEN orders.amount END)" }]
custom_extensions:
- vendor_name: DATUS
data: '{"v": "1.4", "requires": ["measure"], "measure": {"name": "orders_new_product_amount_sum"}}'
4. 优先级与默认值汇总¶
| 关注点 | OSI 核心 | Datus 扩展 | 不存在时的引擎默认 |
|---|---|---|---|
| 关系的连接类型 | 未定义(假定 LEFT) | D-JOIN join_type |
left |
| 缺失分组的指标值 | 未定义 | D-FILL fill_nulls_with |
计数→0(S-FAN-6),其余 NULL |
| 主时间维度 | 未定义(只有 is_time) |
D-TIME time_dimension(指标 > 数据集) |
数据集里唯一的 is_time 字段,否则无 |
| 原生时间粒度 | 未定义 | D-GRAIN time_granularity |
未知 —— 任意粒度均可 |
COUNT(*) 的归属数据集 |
在多数据集模型里未定义 | D-DATASET dataset |
由 SQL 推导,否则 count_star_needs_dataset |
| 窗口派生 | 无法表达(窗口 SQL 被拒绝) | D-WINDOW window |
朴素的基础聚合 |
5. 校验与引擎模式¶
- OSI 关卡(
scripts/validate_osi.py):不受影响 ——custom_extensions是合法 OSI,所以所有 fixture 仍然能 PASS ossie。 - Datus 自校验(引擎处于
--osi-datus模式,即默认模式,且读到一条DATUS条目时):载荷必须是一个 JSON 对象;已知键必须匹配其声明的类型/枚举; 否则invalid_datus_extension。未知键放行(前向兼容), 除非它被requires点名。信封的版本闸门(§2)在这里运行,每个载荷一次 —— 包括SemanticModel这个载体,它自己不定义任何键,但仍要遵守信封规则。 - 基础模式(
--osi-basic):引擎把DATUS当作任何未知厂商来对待 —— 扩展是惰性的(符合规范的原样透传),编译过程会为每个携带它的元素收集一条ignored_vendor_extension告警 —— SemanticModel、Dataset、Field、 Relationship、Metric 一视同仁 —— 并点名改为适用的那个默认值 (D-JOIN → LEFT,D-FILL → 除 S-FAN-6 外不做填充, D-TIME → 只做单is_time推断,D-GRAIN → 任意粒度, D-DATASET → 只做 SQL 推导的归属,D-WINDOW → 朴素的基础聚合)。 载荷不会被解析,所以格式不对的载荷同样只是被忽略并告警 —— 而且在基础模式下根本不会产生任何版本诊断。同一个模型文件在两种模式下都合法; 区别只在语义。(单is_time的主时间推断不是扩展 —— 它在基础模式下同样适用,所以只要纯 OSI 能确定一条唯一的时间轴,metric_time就仍然可用。) - 实现边界:所有扩展读取逻辑 —— 以及模式分支本身 —— 都住在一个模块里,
crates/dosi-compiler/src/ext.rs。将来的扩展也以同样的形态落在那里; 引擎的其余部分对扩展一无所知。
6. 非目标/边界¶
- Datus 扩展绝不重新定义一个 OSI 对象的身份或一个指标的 SQL —— 只在 OSI 未定义的接缝处影响引擎行为。
- 它们不是夹带厂商 SQL 的地方。指标表达式仍然放在 OSI 的
expression.dialects里;引擎依旧从那段 SQL 推断指标语义。 - 那些本该属于 OSI 核心的语义(粒度、时间轴表、窗口/累计指标)
已在
../design/rfc-time-semantics.md里起草并提交上游; 一旦在那里被采纳,对应的 Datus 扩展(D-GRAIN、D-WINDOW)就会退役, 改用核心字段。
7. 变更记录¶
每个已发布的小版本,以及它新增的键。这张表的机器可读形式是
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.0(2026-07-11):初版 ——
D-JOIN(关系的join_type, 已实现:dosi-compiler 把它读进RelationshipIr.join_kind, 规划器据此发出 INNER/LEFT)、D-FILL(指标的fill_nulls_with, 2026-07-12 已实现:MetricIr.fill_nulls_with→ 规划器对指标输出做 COALESCE; 覆盖 S-FAN-6 的计数默认值,对任何种类的指标都适用)。 - 1.1(2026-08-02):时间与归属 ——
D-TIME(数据集/指标的time_dimension,已实现:DatasetIr.primary_time_dimension+MetricIr.time_dimension,保留的metric_time查询名, 按分支的时间范围回退)、D-GRAIN(字段的time_granularity, 已实现:FieldIr.time_granularity,更细粒度的请求 →grain_too_fine)、D-DATASET(指标的dataset,已实现:COUNT(*)的归属)。 基础模式的ignored_vendor_extension告警现在覆盖全部五种载体对象 (此前只有 Relationship 和 Metric)。 - 1.1(2026-08-04,未提升版本号):信封版本闸门本身 ——
v现在会被读取并强制执行(§2),引入了requires, 每个键都带上了声明的忽略影响,整份登记表通过dosi info、GET /v1/capabilities和dosi_engine.DATUS_EXT对外发布。 没有新增或改变任何键,所以版本保持在 1.1。不带v的载荷 —— 也就是 Datus Agent 今天输出的那种 —— 不受影响。 - 1.2(2026-08-06):窗口指标 ——
D-WINDOW(指标的window, 已实现:MetricIr.window→ 规划器的WindowStage投影根; pop/rolling/cumulative 语法糖 + 通用的 offset/frame 形态, 带扫描扩展 + 窗口后裁剪的累计重置;忽略影响为documented—— 指标退回到它朴素的基础聚合;规范见window-extension.md)。 把 13 个 baisheng 的派生时间用例从 Deferred 提升为 Supported(55/2)。 最初只在 DuckDB 上执行;全方言启用(按方言的偏移移位下降、 全方言快照、真机语料验证、datus_window语料场景)作为后续落地 —— 各方言状态见../design/window-metrics-v2.md§1。 族内新增(同一小版本,在早于它们的引擎上会 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: 未知的顶层族键会落进"恰好一个族"的错误,未知的族内键会撞上deny_unknown_fields,未知的函数名会在枚举解析时失败 —— 而且声明"v": "1.3"本身还会触发版本闸门。规范见window-extension.md(各族章节 + 函数登记表)。 - 1.4(2026-08-12):派生指标与度量命名 ——
D-DERIVEM1 (derive键,已实现:filter+compose两族、带成环检测的resolve_metric_refs阶段、表达式等价性闸门、编译期一致维度集, 以及在每个宿主面上暴露的 derive 元数据;reagg/ 跨数据集的filter谓词 /window.base属保留形态,会以not_implemented拒绝并点名各自的里程碑 —— 注意window.base此前会落进"未知键静默忽略"那一档,现在改为 fail closed), 以及D-MEASURE(measure键,已实现:显式度量命名, 按签名去重和冲突检查保持不变 —— 这是对非 ASCII stem 冲突的修复, 也是第一个silent影响级别的键:请把它列进requires)。规范见design/rfc-derived-metrics.md、design/rfc-minimal-declarations.md。
尚未实现(路线图)¶
排队中的这些 RFC 全部是增量的,所以每一个都以一次小版本提升落地, 带上自己的登记表行。顺序可能变化;每一条都独占一个小版本。
| 小版本 | RFC | 键 | 载体 | 影响 | 需要 requires? |
|---|---|---|---|---|---|
| 1.5 | rfc-semi-additive-metrics §4 |
semi_additive |
Metric | silent —— 期末余额会被跨快照加总 |
是 |
| 1.6 | rfc-minimal-declarations §3 D-DISTINCT-STATE |
distinct_state |
Field / Dataset | silent —— 一个物化的 bitmap 列会被当作普通列聚合 |
是 |
| ≥1.5 | rfc-derived-metrics M2-M4 |
window.base、derive 的 reagg 与跨数据集 filter |
Metric | degraded/错误 —— 保留形态今天就以 not_implemented 拒绝,绝不静默计算 |
否 |
| — | window-extension.md 的 rank/share/streak 族、W2+ 函数 |
将来的 window 形态(族内新增,在旧引擎上 fail-closed) |
Metric | degraded |
否 |
第一个 silent 键在 1.4 发布(measure,D-MEASURE)。如果你在一个会输出
silent 影响级别键的 Datus Agent 上编写模型,请留意它产生的 requires 列表:
正是它阻止了更旧的引擎悄悄返回错误数字,而这之所以行得通,
是因为 requires 本身在 1.1 就已发布 —— 早于第一个需要它的键。