REST API¶
dosi-server(crate crates/dosi-server)
通过 HTTP 提供一份 Apache Ossie(原 OSI)语义模型的服务:校验模型、浏览编译后的语义层、
把指标查询编译成方言 SQL,并拿到数仓上执行。只用 JSON,
与 dosi --format json 是同一套机器契约。每个端点对应的 shell 用法见
CLI 参考。
交互式文档:每个运行中的服务都会在
/openapi.json 上托管自己的
OpenAPI 3.1 规范,并在 /docs 上提供 Swagger UI
(用 utoipa 生成;UI 资源已内置,
所以两者都能离线使用)。这一页是配套的叙述性说明;权威的 schema 是那份规范。
启动服务¶
# compile-only, in-memory DuckDB for execution
export DOSI_EXAMPLES=~/.local/share/dosi/examples
dosi-server --model $DOSI_EXAMPLES/orders/model.yaml
# with warehouse connectors and a connections file (a `datasources:` YAML
# in Datus agent.yml vocabulary, or a full agent.yml)
dosi-server \
--model model.yaml --connections dosi-connections.yaml --bind 0.0.0.0:8080
这个路径就是安装脚本放内置示例模型的地方; 换成任何一份 Ossie 模型文件都一样能跑。
| 参数 | 环境变量 | 默认值 | 含义 |
|---|---|---|---|
--model |
DOSI_MODEL |
必填 | Ossie 模型文件(.yaml/.json) |
--connections |
DOSI_CONNECTIONS |
./dosi-connections.yaml → ~/.config/dosi/connections.yaml → ./conf/agent.yml → ~/.datus/conf/agent.yml(旧的 osi- 路径仍会被发现) |
数仓配置,用 Datus agent.yml 的 datasources: 词汇表(connectors.md) |
--bind |
DOSI_BIND |
127.0.0.1:8081 |
监听地址 |
--db |
— | 内存 | 无连接执行时使用的 DuckDB 文件 |
--max-concurrent-executions |
DOSI_MAX_EXECUTIONS |
16 | 执行的并发上限 |
--pool-size |
DOSI_POOL_SIZE |
8 | 每份配置的连接池上限 |
--execute-timeout-secs |
— | 60 | 单次数仓执行的时间预算 |
--request-timeout-secs |
— | 30 | 非执行类请求的时间预算 |
--auth-token |
DOSI_SERVER_TOKEN |
关闭 | 要求 /v1/* 携带 Authorization: Bearer <token> |
--disable-execute |
— | 关闭 | 只编译的部署方式(execute → 403) |
--osi-datus / --osi-basic |
— | --osi-datus |
引擎模式,作用于整个服务:datus 采纳 DATUS custom_extensions;basic 是严格的标准 Ossie,扩展会被忽略、在启动时记为警告,并由 /v1/validate 报告(cli.md) |
模型在启动时加载、校验并编译一次,无效就直接退出进程; 编译出的 IR 以不可变的方式在各请求间共享。数仓执行器按配置各建一次, 所以 MySQL 家族和 Postgres 的连接是池化复用的。
端点¶
| 方法 | 路径 | 用途 |
|---|---|---|
| GET | /health |
存活探测,永不需要鉴权 |
| GET | /ready |
就绪探测 |
| GET | /docs、/openapi.json |
Swagger UI / OpenAPI 规范,永不需要鉴权 |
| GET | /v1/model |
模型名、路径、引擎 mode、对象数量、datus_ext_version |
| GET | /v1/capabilities |
引擎/Ossie 规范/datus-ext 版本,以及本引擎读取的每个 DATUS 扩展键,附带引入它的版本和忽略它的代价(datus-extensions.md) |
| GET | /v1/datasets |
数据集及其来源、键、字段数 |
| GET | /v1/metrics |
指标及其推断出的种类和数据集 |
| GET | /v1/dimensions[?metric=<名称>] |
维度(dataset.field)及时间标记、is_dimension、source;给了 metric 就只返回该指标能分组的字段 |
| GET | /v1/connections |
{name, dialect, available},绝不返回接入点或密钥 |
| POST | /v1/validate |
校验内联的模型文本(按服务当前的引擎模式;响应包含 warnings) |
| POST | /v1/query/compile |
指标查询 → SQL |
| POST | /v1/query/explain |
指标查询 → 逻辑计划(文本) |
| POST | /v1/query/execute |
指标查询 → 结果行 |
| POST | /v1/query/attribute |
指标 + 维度 + 两个窗口 → 指标变化的按维度分解(归因分析) |
| POST | /v1/select/compile |
明细查询 → SQL(明细查询) |
| POST | /v1/select/explain |
明细查询 → 逻辑计划(文本) |
| POST | /v1/select/execute |
明细查询 → 结果行 |
查询请求体¶
compile、explain 和 execute 共用同一个请求体:一个 MetricQuery,
外加 dialect(默认 duckdb)、pretty,以及仅 execute 才有的 connection
(一个配置名;省略 = 本地 DuckDB)。
{
"metrics": ["revenue", "order_count"], // required
"group_by": [
{"field": "customers.region"}, // dataset.field
{"field": "orders.order_date", "grain": "month"} // day|week|month|quarter|year
],
"where_sql": "status = 'completed' AND revenue > 100", // 引用所选指标的条件筛结果行,其余筛输入行(语义 §8)
"context_filter": "customers.region <> 'test'", // 永远在聚合前(参与计算的人群)
"time_range": {"start": "2024-01-01", "end": "2025-01-01",
"dimension": "orders.order_date"}, // half-open [start, end)
"order_by": [{"key": "order_date__month", "desc": true}],
"limit": 12,
"params": {"n": [7, 30]}, // D-PARAM 绑定:每个名字一个值或一个列表
"dialect": "postgres",
"connection": "warehouse-prod" // execute only
}
$ curl -s localhost:8081/v1/query/compile -H 'content-type: application/json' \
-d '{"metrics":["revenue"],"group_by":[{"field":"orders.order_date","grain":"month"}],"dialect":"postgres"}'
{"dialect":"postgres","sql":"SELECT DATE_TRUNC('MONTH', orders.order_date) AS order_date__month, ..."}
$ curl -s localhost:8081/v1/query/execute -H 'content-type: application/json' \
-d '{"metrics":["revenue"],"group_by":[{"field":"customers.region"}],"connection":"pg"}'
{"dialect":"postgres","sql":"...","columns":["region","revenue"],
"rows":[{"region":"east","revenue":340},{"region":"west","revenue":110}],"row_count":2}
$ curl -s localhost:8081/v1/validate \
-d "$(jq -Rs '{model: .}' < model.yaml)" -H 'content-type: application/json'
{"valid":true,"issues":[],"compile_errors":[]}
明细请求体¶
另一个查询平面返回记录而不是聚合值。它的请求体就是 DetailQuery 这个 wire
类型,CLI、MCP、Python 三个接口面完全一致:
{
"from": "orders", // 结果粒度:一行一个订单
"fields": [ // "field" | "dataset.field" | "rel.rel.field",
"orders.order_id", // 可加 ":grain" 后缀
"orders.amount",
"customers.name"
],
"where_sql": "customers.tier = 'Enterprise'",
"time_range": {"start": "2026-01-01", "end": "2026-02-01"},
"order_by": [{"key": "amount", "desc": true}], // 只能是已投影的列
"limit": 100, // 默认 100,截到 10000
"dialect": "postgres", // 与上面相同的 execute/compile 外层字段
"connection": "warehouse-prod"
}
$ curl -s localhost:8080/v1/select/execute \
-H 'content-type: application/json' \
-d '{"from":"orders","fields":["orders.order_id","customers.name"],"limit":10}'
字段、过滤或时间范围引用到 from 以外的数据集时会自动连接,结果粒度仍是
from。只能逆着连接方向抵达的数据集返回 detail_fanout,并给出应该改用的
根;多条路径都能抵达的字段返回 ambiguous_join_path,并给出点名各条路径的
关系前缀。
Arrow IPC 流式传输¶
POST /v1/query/execute 带上 Accept: application/vnd.apache.arrow.stream
时,会以流式的 Arrow IPC body 而不是 JSON 返回结果:
record batch 从数仓适配器经由 IPC writer 直接流进响应,
既不做行物化,也不逐值做 JSON 编码。列式消费方
(Polars、pandas/pyarrow、DataFusion、另一个 Dosi)可以零解析地读取;
收益随结果规模放大。对不主动选择它的客户端,JSON 响应逐字节保持不变。
$ curl -s localhost:8081/v1/query/execute -H 'content-type: application/json' \
-H 'accept: application/vnd.apache.arrow.stream' \
-d '{"metrics":["revenue"],"group_by":[{"field":"customers.region"}]}' \
| python3 -c 'import pyarrow.ipc,sys; print(pyarrow.ipc.open_stream(sys.stdin.buffer).read_all())'
语义:
- 预检阶段的错误仍走 JSON 信封:编译、配置、数仓层面的失败都在响应提交之前
发现,所以拿到的仍是结构化的
{"error": {code, message, hint}}和恰当的 HTTP 状态码。 - 流中途的失败会直接终止 body,这是流式 API 的常规做法: IPC 流在没有结束标记的情况下中止,重新发起请求即可。
- 这条通路上,
--execute-timeout-secs限制的是首字节时间, 不是整个流的时长;并发许可会一直持有到流结束。 - CLI 上的等价物是
dosi query --execute --format arrow(IPC 输出到 stdout)。 - 可用性:默认开启(服务端 feature
arrow)。Flight SQL 的服务端端点 属于后续工作,目前的列式出口就是这个 REST body。
错误¶
错误响应体原样包裹引擎的结构化错误,机器码与 dosi --format json 完全一致且稳定:
{"error": {"code": "unknown_metric",
"message": "unknown metric \"revenu\"",
"candidates": ["revenue", "order_count", "..."]}}
| HTTP | 什么时候 |
|---|---|
| 400 | 规划器拒绝(unknown_metric、ambiguous_dimension、no_join_path 等)、坏 JSON、未知方言、配置 config 错误 |
| 401 | 缺少或错误的 bearer token(仅当设置了 --auth-token 时) |
| 403 | 在 --disable-execute 下访问 /v1/query/execute |
| 429 | 所有执行槽位都忙(Retry-After: 1) |
| 501 | 规划器的 not_implemented |
| 502 | 数仓不可达/凭据被拒/SQL 被拒(connection、auth、sql_rejected、driver) |
| 504 | 数仓 timeout,或服务自身的执行超时 |
并发模型¶
- 编译、explain、列表都是纯 CPU 操作,作用在共享的内存 IR 上, 直接在异步工作线程上内联执行,无锁、无 I/O。release 构建可以在 p50 个位数毫秒的水平上持续支撑每秒数千次编译请求。
- 执行是阻塞式的数仓 I/O:由信号量(
--max-concurrent-executions)限流, 用spawn_blocking挪出异步工作线程,--execute-timeout-secs到点后 放弃响应。已知的 v1 限制:被放弃的调用仍会在后台跑完, 期间一直占着许可。 - MySQL 家族和 Postgres 的配置会池化连接(
--pool-size按配置计), ClickHouse/Trino 复用 HTTP keep-alive,DuckDB 每次调用起一个子进程。