跳转至

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 每次调用起一个子进程。