错误码¶
所有表面——CLI、REST、MCP、Python——都用同一套结构化错误报告失败。
错误码是稳定 API,错误文本不是:按 code(snake_case)匹配,读
candidates / suggested_retry / hint 来自纠错,不要解析文本。
错误的结构¶
错误沿流水线分阶段,每个阶段一个错误码枚举:
| 阶段 | 何时运行 | payload 形态 |
|---|---|---|
| load | 读取模型文件 | 目前只有文本(无 code) |
| validate | 模型结构检查 | {severity, code, location, message} |
| compile | 把模型降解为 IR | {code, location, message, candidates?, hint?} |
| plan | 编译指标查询 | {code, message, metrics?, candidates?, suggested_retry?} |
| execute | 在数仓上执行 SQL | {code, message, hint?} |
可选字段为空时省略,payload 保持紧凑。location 指出该改哪里
(metric 'revenue'、semantic_model[0].relationships[2].to);
candidates 列出错误引用的合法替代;suggested_retry / hint
给出能成功的改写。
各表面:
- CLI ——
--format json把错误结构体以 JSON 打到 stderr;validate --format json在 stdout 输出{issues, compile_errors, warnings}。退出码:0成功,1引擎拒绝(结构化错误),2用法错误。已知不对称:load 错误和 非validate命令下的校验问题只有文本形态。 - REST 与 MCP —— 同一个 body,外面包一层:
{"error": {…}}。MCP 工具失败时把它作为isError: true的文本内容返回;MCP 工具参数不合法时返回的是纯文本 schema 错误。 - Python(
dosi_engine)——ModelError/QueryError/ExecuteError,都有.code与.to_dict();planner 的suggested_retry到 Python 侧叫hint。
{"error": {"code": "unknown_metric",
"message": "unknown metric \"revenu\"",
"metrics": ["revenu"],
"candidates": ["revenue", "order_count", "..."]}}
各命令可能出现的错误码¶
| 表面 | validate issues | compile 错误 | plan 错误 | execute 错误 |
|---|---|---|---|---|
CLI validate |
✓(issues) |
✓(compile_errors) |
— | — |
CLI query / explain / list |
✓(文本,中止) | ✓ | ✓ | 仅 query --execute |
REST POST /v1/validate、MCP validate_model |
✓(在 body 中) | ✓(在 body 中) | — | — |
REST /v1/query/compile · /explain、MCP compile_sql · explain_query |
—(模型启动时加载) | ✓(惰性编译) | ✓ | — |
REST /v1/query/execute、MCP run_query |
— | ✓ | ✓ | ✓ |
Python Engine(...) |
ModelError("invalid_model") |
ModelError("compile_error") |
— | — |
Python compile_sql / query |
— | — | QueryError |
ExecuteError |
服务级错误码(bad_request、unauthorized、forbidden、busy、
internal、timeout)可伴随任意 REST/MCP 调用出现,见
服务级错误码。
模型校验问题¶
结构检查(dosi validate、POST /v1/validate)。error 使模型无效;
warning 不会——模型仍可编译。
| 错误码 | 级别 | 何时触发 | 修复 |
|---|---|---|---|
unsupported_version |
error | version 不等于本引擎实现的 Apache Ossie 规范版本 |
按消息中的版本号钉住 |
duplicate_name |
error | 模型、数据集、字段、指标或关系重名 | 重命名其一 |
empty_datasets |
error | 语义模型没有数据集 | 至少加一个数据集 |
empty_columns |
error | 关系未声明任何列对 | 填写 from_columns / to_columns |
column_count_mismatch |
error | from_columns 与 to_columns 长度不等 |
二者按位置配对——改成等长 |
unknown_dataset |
error | 关系端点引用了未声明的数据集 | 修正 from: / to: 名称 |
unknown_column |
error | 关系列不是所在数据集声明的字段 | 声明该字段或改列名 |
missing_sql_dialect |
error(指标)/ warning(字段) | 缺少 ANSI_SQL(或其他 SQL)方言条目 |
补一条 SQL 方言表达式 |
self_relationship |
warning | 关系把数据集连到自身 | 通常是有意的;复查即可 |
relationship_target_not_unique |
warning | to_columns 不是目标的主键/唯一键——join 可能发散 |
让 join 指向唯一键;若是有意的事实表配对,声明 cardinality: many_to_many |
{"severity": "error", "code": "unknown_dataset",
"location": "semantic_model[0].relationships[0].to",
"message": "relationship \"orders_to_customers\" references unknown dataset \"customers\""}
编译错误¶
把有效模型降解为 IR(validate 信封中的 compile_errors;查询表面上在模型
首次编译时出现)。全部携带 location;candidates / hint 见备注。
| 错误码 | 何时触发 | payload 附加 |
|---|---|---|
unparseable_expression |
表达式不是可解析的 SQL | |
no_sql_dialect |
被使用的字段/指标没有 SQL 方言条目 | |
unknown_dataset |
SUM(no_such.amount)——限定名不是数据集;另外在文档内语义模型数量 ≠ 1 时也被(误)用 |
candidates:数据集名 |
unknown_field |
dataset.field 中数据集没有该字段 |
candidates:该数据集的字段 |
unknown_column |
裸列在任何数据集都找不到 | hint:限定为 dataset.field |
ambiguous_column |
裸列存在于多个数据集 | candidates:限定拼法 |
not_an_aggregate |
指标表达式没有聚合 | hint:包一层 SUM(...) |
nested_aggregate |
SUM(MAX(...)) |
|
bare_column_in_metric |
指标里出现未聚合的列(或纯字面量聚合) | hint:对数据集字段聚合 |
cross_dataset_aggregate |
一个聚合混用两个数据集的列 | 拆开聚合 |
count_star_needs_dataset |
多数据集模型里的裸 COUNT(*) |
hint:改数键列 |
unsupported_aggregate |
SUM/COUNT/COUNT DISTINCT/AVG/MIN/MAX 之外的聚合 | |
multi_column_count_distinct |
已声明但目前不会产生 | |
window_in_metric |
指标表达式里的窗口函数 | hint:在数据集里预计算 |
subquery_in_metric |
指标表达式里的子查询 | |
measure_name_collision |
两个不同聚合合成了同名 measure | 部分触发点带 hint |
metric_reference_cycle |
D-DERIVE compose 指标互相引用 | 消息展示环路路径 |
derive_expression_mismatch |
手写 fallback 表达式与 derive 展开不一致 | hint |
invalid_datus_extension |
DATUS payload 不合法(v 错、字段引用错等) |
hint 带文档锚点 |
unsupported_datus_ext_version |
payload 的 v 是更新的 major / 低于 1.0 |
hint 指出支持的 major |
datus_ext_key_required |
requires: [...] 点名了本引擎未实现的键 |
|
not_implemented |
声明合法但超出当前 derive/window/LOD 范围 | hint 带文档锚点 |
编译警告¶
非致命:模型编译通过,但并非所有声明都生效。形态
{code, location, message};CLI 在 stderr 打 ! … 且退出码为 0。
| 错误码 | 何时触发 |
|---|---|
ignored_vendor_extension |
basic(标准 Ossie)模式忽略了 DATUS 扩展 |
datus_ext_version_ahead |
payload 声明了更新的 minor;未知键被丢弃(消息中点名) |
datus_ext_key_newer_than_declared |
用了比声明的 v 更新的键,但仍被采纳 |
datus_ext_requires_missing |
使用了非空的 time、measure 等 silent 扩展,却未列入 requires;补齐后可防止旧引擎忽略它 |
compose_no_conformed_dimensions |
compose 指标的成员没有共同维度——任何查询都无法分组 |
datus_time_format_misplaced |
时间维度的顶层 format 写的是存储布局(%Y%m%d 等);它是展示元数据,已被忽略——布局应写进 time 块(D-FORMAT) |
datus_param_unbounded |
D-PARAM 的 int/float 参数没声明 allowed/min/max |
datus_param_unused |
声明了却没有任何槽位引用的参数 |
underdeclared_field |
(datus 模式,仅 dosi validate)字段没写 is_dimension,但看起来是度量、业务键或没标记的时间列,因而会被当作分组维度推荐 |
dimension_role_conflict |
(datus 模式,仅 dosi validate)字段声明了 is_dimension: true,却被某个指标当度量聚合;声明照常生效,消息会点出是哪个指标 |
datus_param_opaque_token |
allowed 里的取值经 ASCII 词根消毒后无法区分,其输出列改用 h<hex> 后缀(消息里给出对应关系) |
aggregate_field_dropped |
字段表达式里有聚合,却没有声明 D-LOD 粒度——字段被丢弃,引用它的地方以 unknown_field 失败 |
查询规划错误¶
两个查询平面的拒绝:指标平面(dosi query、/v1/query/*、compile_sql /
run_query)与明细平面(dosi select、/v1/select/*、compile_select /
select)。HTTP 一律 400,例外:not_implemented → 501、
internal → 500。
| 错误码 | 何时触发 | payload 附加 |
|---|---|---|
empty_query |
未请求任何指标 | candidates:全部指标 |
dimensions_required |
归因请求没给 dimensions;引擎不猜哪些列能解释这次变化 |
metrics、candidates:该指标值得拆解的维度(好的在前)、suggested_retry:可直接粘贴的 dimensions 片段 |
unknown_metric |
指标名不在模型里 | metrics、candidates |
unknown_dimension |
group-by 项不是维度 —— 包括写了一个指标名,此时 retry 会告诉你该怎么请求它;窗口指标的时间轴缺席 group-by 时同码 | candidates 或 suggested_retry |
ambiguous_dimension |
裸字段存在于多个数据集 | candidates:限定拼法 |
grain_on_non_time_dimension |
对非时间字段加 :grain |
suggested_retry |
grain_too_fine |
请求的粒度细于存储的 time_granularity;或时间范围的边界落在 time.format 列无法细分的周期内部 |
suggested_retry |
duplicate_output_name |
两个 group-by 项——或两个指标列——产出同名输出列 | suggested_retry |
unknown_order_key |
排序键既不是指标也不是 group-by 项;列表绑定下的指标名(摊成多列)同码 | candidates:可用的键,或各组绑定 + suggested_retry |
unsupported_filter |
where 中包含子查询/窗口、where_sql 引用了 D-FORMAT 编码字段、time_ranges 下直接写了指标名(应写 {metric}__{tag}),或时间范围格式不对 |
相关时 candidates 列出带 tag 的名字。编码时间字段改用 time_range。 |
aggregate_in_where |
where 里出现内联聚合 |
suggested_retry:声明成指标、加入 metrics,再在 where_sql 里按名引用(过滤条件) |
result_filter_metric_not_selected |
where_sql 引用了查询没有选择的指标;指标条件筛选的是本次查询自己的结果行 |
metrics、candidates:已选的指标、suggested_retry |
result_filter_unprojected |
where_sql 的同一个子条件既引用指标又引用未分组的字段,没有哪个位置能同时求值两者 |
suggested_retry:把字段加入分组,或拆成两个子条件 |
no_join_path |
没有数据集能沿多对一同时到达 measure 与全部 group-by | metrics |
ambiguous_join_path |
存在多条关系路径——语义会不同 | candidates:各路径、suggested_retry |
fan_out_risk |
对重复敏感的聚合跨越会发散的 join | metrics、suggested_retry |
time_range_needs_dimension |
作用域里有多个时间维度,范围目标不明 | candidates |
no_primary_time_dimension |
用了 metric_time 但没有声明主时间维度 |
candidates、suggested_retry |
metric_time_conflict |
已不再产生:同一底表上时间轴不同的指标现在每条轴一个分支(见语义);保留这个错误码是为了兼容已有客户端 | |
unconformed_dimension |
维度在派生指标的 conformed 集合之外 | metrics、candidates、suggested_retry |
unknown_metric_param |
params 里的名字没有任何被查指标声明 |
candidates:已声明的名字、suggested_retry |
param_out_of_domain |
绑定越出参数的类型/取值域、列表为空或有重复、或在只允许单值处(归因)传了列表 | metrics、suggested_retry:一段合法的 params |
param_expansion_too_large |
列表绑定展开超过 64 个输出列 | metrics、suggested_retry |
window_reset_too_fine |
窗口 reset 细于查询粒度 | suggested_retry |
window_exclude_not_grouped |
依赖链上某个窗口 partition.exclude 的维度不在查询的 group_by 里 |
metrics:依赖链缺维度的那些被请求指标(具体是哪一层在 message 里);candidates:本次查询的维度;suggested_retry:一次性列出需要补上的全部维度 |
dialect_unsupported_window_function |
目标方言缺少所需窗口函数 | metrics、suggested_retry |
unknown_dataset |
明细查询的 from 不是任何数据集 |
candidates:数据集名 |
detail_fanout |
明细查询只能逆着连接方向抵达某数据集,一行根会变成好几行 | candidates:反向路径;suggested_retry:应改用的根 |
metric_in_detail_query |
指标出现在明细查询的投影里 | metrics、suggested_retry:对应的 dosi query 调用 |
unknown_relationship |
关系前缀引用里有不存在的关系名 | candidates:从当前数据集出发的边;suggested_retry |
invalid_relationship_path |
命名路径接不上、重复访问某数据集,或不从查询的基础数据集出发 | candidates:该数据集的出边;suggested_retry |
not_implemented |
模型层面合法,超出当前引擎范围——包括一次查询用两条路径抵达同一数据集,以及超出当前里程碑的 D-LOD 形态(跨层 OR、窗口指标、跨事实对齐) |
metrics 或 candidates、suggested_retry |
internal |
引擎不变量被破坏——重试无用;请报 bug |
典型的自纠错循环:unknown_metric → 从 candidates 里选 → 重试;
fan_out_risk → 按 suggested_retry(按可达维度分组,或改成
COUNT DISTINCT/MIN/MAX):
{"code": "fan_out_risk",
"message": "measure \"views_source_views_sum\" aggregates Sum over dataset \"views_source\", which has no many→one path to [\"bookings_source\"]; evaluating a duplicate-sensitive aggregate (SUM/AVG/COUNT) across that join would double-count rows",
"metrics": ["views_times_booking_value"],
"suggested_retry": "group by dimensions reachable from the measure's dataset, or restate the metric with a duplicate-insensitive aggregate (COUNT DISTINCT/MIN/MAX)"}
明细平面的两个循环同理:detail_fanout 直接给出能在正确粒度上回答该问题的
根数据集,ambiguous_join_path 直接给出点名各条候选路径的关系前缀,
两者都可以粘贴进下一次调用。参见明细查询。
执行错误¶
在数仓上执行编译出的 SQL(query --execute、/v1/query/execute、
run_query)。形态 {code, message, hint?}。
| 错误码 | HTTP | 何时触发 |
|---|---|---|
config |
400 | 连接配置有问题(URI 错、DuckDB 文件打不开、驱动选项缺失) |
connection |
502 | 数仓不可达 |
auth |
502 | 凭据被拒 |
sql_rejected |
502 | 数仓拒绝了 SQL(表缺失、类型错误)——消息内嵌数仓自身的报错 |
driver |
502 | 连接建立后驱动层失败 |
timeout |
504 | 语句或服务端执行超时 |
服务级错误码¶
dosi-server 在同一信封里追加传输层错误码,
{"error": {"code", "message"}}:
| 错误码 | HTTP | 何时触发 |
|---|---|---|
bad_request |
400 | JSON body 不合法、方言未知 |
unauthorized |
401 | bearer token 缺失/错误 |
forbidden |
403 | --disable-execute 下调用执行 |
busy |
429 | 执行槽位占满(Retry-After: 1) |
timeout |
504 | 服务端自身的执行超时 |
internal |
500 | 意料之外的服务失败 |
Python 异常¶
dosi_engine wheel 抛一套异常层级(基类 OsiError,均有 .code 与
.to_dict()):
| 异常 | 错误码 | 说明 |
|---|---|---|
ModelError |
io、parse、ontology、invalid_model、compile_error |
invalid_model 带 issues;compile_error 带 compile_errors 及首个错误的 candidates/hint;ontology(ontology 无法解析或降低)在消息里列出每条拒绝及其自身的错误码 |
QueryError |
上文全部 plan 错误码 | suggested_retry 到达时名为 hint |
ExecuteError |
上文全部 execute 错误码 |
编译警告在 Engine(...) 构造时以 Python RuntimeWarning 抛出——
python -W error 可把它们升级为失败。
SQL 生成错误(parse_error、unsupported_aggregate 等)是 planner 与
渲染器之间的内部边界,只会出现在上述 compile 或 plan 错误内部,
不构成独立的用户级契约。
本页每个错误码都有测试钉住:fixtures/errors/ 的负向用例目录随每次提交
运行,实际 payload 可在 test-status 看板的 "Error contract" 区浏览。