REST API¶
dosi-server(crate crates/dosi-server)
通过 HTTP 提供一份 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
cargo run -p dosi-server -- --model fixtures/orders/model.yaml
# with warehouse connectors and a connections file (a `datasources:` YAML
# in Datus agent.yml vocabulary, or a full agent.yml)
cargo run -p dosi-server --features exec-all -- \
--model model.yaml --connections dosi-connections.yaml --bind 0.0.0.0:8080
| 参数 | 环境变量 | 默认值 | 含义 |
|---|---|---|---|
--model |
DOSI_MODEL |
必填 | OSI 模型文件(.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 是严格的标准 OSI,扩展会被忽略、在启动时记为警告,并由 /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 |
引擎/OSI 规范/datus-ext 版本,以及本引擎读取的每个 DATUS 扩展键,附带引入它的版本和忽略它的代价(datus-extensions.md) |
| GET | /v1/datasets |
数据集及其来源、键、字段数 |
| GET | /v1/metrics |
指标及其推断出的种类和数据集 |
| GET | /v1/dimensions |
维度(dataset.field)及时间标记 |
| GET | /v1/connections |
{name, dialect, available},绝不返回接入点或密钥 |
| POST | /v1/validate |
校验内联的模型文本(按服务当前的引擎模式;响应包含 warnings) |
| POST | /v1/query/compile |
指标查询 → SQL |
| POST | /v1/query/explain |
指标查询 → 逻辑计划(文本) |
| POST | /v1/query/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'", // pre-aggregation SQL filter
"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,
"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":[]}
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 每次调用起一个子进程。