错误码¶
所有表面——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 不等于本引擎实现的 OSI 规范版本 |
按消息中的版本号钉住 |
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 指向唯一键 |
{"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 范围 | hint 带文档锚点 |
编译警告¶
非致命:模型编译通过,但并非所有声明都生效。形态
{code, location, message};CLI 在 stderr 打 ! … 且退出码为 0。
| 错误码 | 何时触发 |
|---|---|
ignored_vendor_extension |
basic(标准 OSI)模式忽略了 DATUS 扩展 |
datus_ext_version_ahead |
payload 声明了更新的 minor;未知键被丢弃(消息中点名) |
datus_ext_key_newer_than_declared |
用了比声明的 v 更新的键,但仍被采纳 |
compose_no_conformed_dimensions |
compose 指标的成员没有共同维度——任何查询都无法分组 |
查询规划错误¶
指标查询被拒绝(dosi query、/v1/query/*、compile_sql /
run_query)。HTTP 一律 400,例外:not_implemented → 501、
internal → 500。
| 错误码 | 何时触发 | payload 附加 |
|---|---|---|
empty_query |
未请求任何指标 | candidates:全部指标 |
unknown_metric |
指标名不在模型里 | metrics、candidates |
unknown_dimension |
group-by 项不是维度;窗口指标的时间轴缺席 group-by 时同码 | candidates 或 suggested_retry |
ambiguous_dimension |
裸字段存在于多个数据集 | candidates:限定拼法 |
grain_on_non_time_dimension |
对非时间字段加 :grain |
suggested_retry |
grain_too_fine |
请求的粒度细于存储的 time_granularity |
suggested_retry |
duplicate_output_name |
两个 group-by 项产出同名输出列 | suggested_retry |
unknown_order_key |
排序键不是输出列 | candidates:输出列 |
unsupported_filter |
where 里的子查询/窗口,或时间范围格式不对 |
|
aggregate_in_where |
where 里出现聚合(暂不支持指标过滤) |
|
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 |
被查询的指标时间轴不一致 | metrics、suggested_retry |
unconformed_dimension |
维度在派生指标的 conformed 集合之外 | metrics、candidates、suggested_retry |
window_reset_too_fine |
窗口 reset 细于查询粒度 | suggested_retry |
dialect_unsupported_window_function |
目标方言缺少所需窗口函数 | metrics、suggested_retry |
not_implemented |
模型层面合法,超出当前引擎范围 | metrics、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)"}
执行错误¶
在数仓上执行编译出的 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、invalid_model、compile_error |
invalid_model 带 issues;compile_error 带 compile_errors 及首个错误的 candidates/hint |
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" 区浏览。