跳转至

CLI 参考

dosi(crate crates/dosi-engine) 是 Dosi 的命令行界面:校验一份 Apache Ossie(原 OSI)语义模型、浏览它编译出的 指标/数据集/维度、把一次指标查询或明细查询编译成方言 SQL, 并把这段 SQL 在数仓上执行。 REST API 通过 HTTP 能做的一切,dosi 都能在 shell 里做: 同一个编译器、同样的结构化错误、同样的 --format json 机器契约。

$ dosi --help
Compile metric queries over OSI semantic models to dialect SQL

Usage: osi [OPTIONS] <COMMAND>

Commands:
  validate  Validate the model: structure, unique names, relationship integrity, and metric compilation
  list      List model objects
  query     Compile a metric query to SQL
  select    Query row-level detail: project fields at one dataset's grain, joining related datasets automatically
  explain   Show the compiled IR shape and logical plan without generating SQL

安装

dosi 发布两种形态:默认版把 DuckDB 官方库 libduckdb 放在可执行文件旁边 —— 这些文件总是一起走,没有别的东西要装;精简版不带它,本地 DuckDB 查询改走 PATH 上的 duckdb 命令行。两种形态都带上了全部数仓连接器。

怎么选、下载命令和校验方式见安装。

Dosi 面向 Linux 和 macOS 发布,可执行文件只依赖标准系统库(Linux 上 glibc ≥ 2.28, macOS 11 及以上);默认版还通过旁边的 libduckdb 带来 libstdc++ 依赖, 精简版则完全没有 C++ 依赖。 连接器里的 TLS 用 rustls,所以不需要安装 OpenSSL,也不需要任何数据库客户端库。

全局选项

以下选项对每条命令都生效(clap 的 global = true):

参数 环境变量 默认值 含义
--model <path> DOSI_MODEL 必填 Ossie 模型文件(.yaml / .yml / .json)
--format <fmt> — text 输出格式:text、json 或 arrow(见输出格式)
--connections <path> DOSI_CONNECTIONS 发现链 --execute 用的连接配置文件(见在数仓上执行)
--osi-datus — 开启 Datus 模式:DATUS custom_extensions 会被采纳(datus-extensions.md)
--osi-basic — 关闭 基础模式,严格的标准 Ossie。DATUS 扩展被忽略并给出警告;validate 还会额外运行上游校验器(见下)

--osi-datus 与 --osi-basic 互斥,同时传入属于用法错误。基础模式下, 每个被忽略的扩展都会在 stderr 上打印一行 ! 警告(在 --format json 下则落在 warnings 里);警告永远不改变退出码。

如果设置了 DOSI_MODEL,--model 可以省略;一条需要模型却两者都没找到的命令会以 no model given; pass --model <path> or set DOSI_MODEL 退出。

退出码: 0 成功,1 引擎拒绝了这次查询/执行(带结构化错误), 2 用法/CLI 错误(参数写错、缺模型、参数无法解析)。

命令

dosi info

报告引擎版本、Ossie 规范版本和 datus-ext 版本,以及本引擎会读取的每一个 vendor_name: DATUS 的 custom_extensions 键。它不需要模型, 所以也可以当作版本探测命令用。

$ dosi info
dosi       0.1.12
osi spec   0.2.0.dev0
datus-ext  1.9 (accepts 1.0 and up)
mode       datus
build      engine (DuckDB in process)
examples   /home/you/.local/share/dosi/examples

KEY               EXTENSION  CARRIERS         SINCE  IF IGNORED
join_type         D-JOIN     relationship     1.0    documented
fill_nulls_with   D-FILL     metric           1.0    documented
time_dimension    D-TIME     dataset, metric  1.1    degraded
time_granularity  D-GRAIN    field            1.1    documented
window            D-WINDOW   metric           1.2    documented
dataset           D-DATASET  metric           1.1    degraded
derive            D-DERIVE   metric           1.4    degraded
measure           D-MEASURE  metric           1.4    silent
params            D-PARAM    metric           1.5    degraded
time              D-FORMAT   field            1.6    silent
is_dimension      D-DIM      field            1.7    documented
cardinality       D-CONFORM  relationship     1.8    silent
lod               D-LOD      field            1.10   degraded

build 说明这是发布的两种形态里的哪一种 —— engine 在进程内跑 DuckDB, lean 把本地 DuckDB 交给 PATH 上的 duckdb 命令行 (见该下载哪个?)。没有这一行, 装好的可执行文件是哪一种就只能靠 ldd 分辨。

examples 是内置示例模型的所在位置:设了 $DOSI_EXAMPLES 就用它, 否则找 $XDG_DATA_HOME/dosi/examples(即 ~/.local/share/dosi/examples)。 两处都没有时这一行不打印 —— 源码检出就是这种情况,同样的模型在 fixtures/orders 和 fixtures/tpcds。本页的示例都用 $DOSI_EXAMPLES, 先导出一次,下面的命令就都能直接复制运行:

$ export DOSI_EXAMPLES=~/.local/share/dosi/examples

SINCE 是引入该键的 datus-ext 版本,IF IGNORED 是不采纳它要付出的代价, 见 datus-extensions.md §2.1。 --format json 以机器契约的形式输出同样的内容。 生成模型的 Agent 在决定输出哪些键之前,应该先读它。

dosi validate

加载模型并跑完整的校验流水线:结构、名称唯一性、关系引用完整性、 指标表达式编译,然后报告每一个问题。

$ dosi validate --model $DOSI_EXAMPLES/tpcds/model.yaml
✓ 1 semantic model(s) valid

--format json 会输出 {issues, compile_errors, warnings} 供 CI 把关: 每个 issue 带严重级别和消息,每个编译错误带稳定的 code 和可选的 hint, 每条警告带稳定的 code、location 和 message。有错误的模型以非零码退出; 只有警告则不会。

字段角色提示(datus 模式)。 在 --osi-datus(默认模式)下,校验还会指出 模型没声明、结构也推断不出来的字段角色:没有任何指标聚合的度量列、既不是声明的键 也不是关系列的业务键、没标 dimension.is_time 的时间列。每条警告都点名字段并给出 该写什么(通常是 is_dimension);不管它,这些字段 就会被当作分组维度推荐出去,agent 真的会按它们分组。这项检查总是执行,没有开关 可以关掉;--strict 让它计入退出码,供 CI 使用:

$ dosi validate --strict --model model.yaml
! underdeclared_field in field 'routes.distance_miles': `routes.distance_miles` looks like a
  measurement no metric aggregates, so it is offered as a grouping dimension; write the
  metric it belongs to, or declare `is_dimension: false`
✗ the authoring lint reported findings and --strict is set

上游校验(--osi-basic)。 在基础模式下,当能找到一份 checkout 时, validate 会额外调用上游的 apache/ossie 参考校验器(即 OSSIE_DIR,默认 ~/src/ossie,其下含有 validation/validate.py), 用已发布的规范来要求这份模型。那个校验器把自己的依赖写成了脚本内联元数据, 所以 PATH 上有 uv 时走 uv run --script, 否则走 python3。上游失败会让整条命令失败;但凡是让校验器根本跑不起来的情况 —— 没有 checkout、没有解释器、或者解释器缺 pyyaml / jsonschema / sqlglot —— 都只打印一条提示、退回到只做内置校验,因为「我们问不到」不能读成「这个模型不合法」。 --format json 会额外输出 "upstream": {ran, passed, output, note}。

请把校验器 checkout 到提交 7b8cdaa,而不是 main:上游开发中的 schema 在 apache/ossie#383 里改成一份文档只放一个不带包装的模型,而本版本只读 semantic_model: 列表写法,所以上游 main 会拒绝本版本接受的所有模型。

$ git clone https://github.com/apache/ossie ~/src/ossie
$ git -C ~/src/ossie checkout 7b8cdaa
$ OSSIE_DIR=~/src/apache-ossie dosi validate --osi-basic --model model.yaml
✓ upstream OSI validation passed
✓ 1 semantic model(s) valid

dosi list <what>

浏览编译后的语义层。三个子命令,在 text 模式下各是一张表, 在 --format json 下则是一个对象数组:

子命令 列
dosi list datasets 名称、来源、主键、字段数、时间维度
dosi list metrics 名称、推断出的种类(aggregate / ratio / expression)、数据集、描述
dosi list dimensions [--metric <名称>] dataset.field、时间标记、是否值得用来分组(DIM)、由谁判定(SOURCE)、描述

不带 --metric 时列出模型的全部字段;带 --metric 时只列出该指标能分组的字段, 推荐的排在前面。DIM 为 no 表示模型声明(或引擎推断)它不是维度 —— 度量列、键、 关系列;SOURCE 说明依据:declared 表示来自 D-DIM 声明,否则是 inferred:<规则>。 no 的字段仍然可以写进 --group-by:这是推荐,不是限制。

$ dosi list dimensions --metric revenue --model $DOSI_EXAMPLES/orders/model.yaml
NAME                   TIME  DIM  SOURCE                DESCRIPTION
metric_time            time  yes  inferred:time
orders.order_date      time  yes  inferred:time
orders.status                yes  inferred
customers.region             yes  inferred
products.category            yes  inferred
orders.amount                no   inferred:measure
orders.customer_id           no   inferred:foreign_key
orders.order_id              no   inferred:primary_key
$ dosi list metrics --model $DOSI_EXAMPLES/tpcds/model.yaml
NAME                     KIND        DATASETS               DESCRIPTION
total_sales              aggregate   store_sales            Total sales revenue across all transactions
total_profit             aggregate   store_sales            Total net profit from store sales
customer_lifetime_value  ratio       customer, store_sales  Average lifetime sales value per customer
sales_by_brand           aggregate   store_sales            Total sales by brand (requires grouping by item.i_brand)
store_productivity       expression  store, store_sales     Sales per employee across stores

dosi query

核心命令:把一次指标查询编译成方言 SQL,并可选地执行它。查询本身由下面共用的 query spec 参数来描述;在此之上,query 还接受:

参数 含义
--dialect <name> 目标 SQL 方言(默认 duckdb,或 --connection 所指配置的方言)
--pretty 美化打印生成的 SQL
--explain 在 SQL 上方一并打印逻辑计划
--execute 把编译出的 SQL 拿到数仓上执行并打印结果行
--connection <name> 连接配置文件里的具名配置(同时决定方言)
--db <path> 不带 --connection 时 --execute 使用的 DuckDB 文件(默认:内存)

只编译(不加 --execute)会打印 SQL(text)或 {dialect, sql}(json):

$ dosi query --model $DOSI_EXAMPLES/tpcds/model.yaml \
    --metrics total_sales \
    --group-by store.s_state,date_dim.d_date:month \
    --where "item.i_category = 'Books'" \
    --start-time 2024-01-01 --end-time 2025-01-01 \
    --order -total_sales --limit 100 \
    --dialect starrocks
SELECT store.s_state AS s_state, DATE_TRUNC('MONTH', date_dim.d_date) AS d_date__month, ...

(把 total_sales 和基于 store 的指标(比如 store_productivity)放在一起, 会得到 fan_out_risk 错误:按门店计算的度量,无法按门店够不到的日期来分组。 这种保护正是要点所在,见 semantics.md §6。)

query spec 参数(与 explain 共用)

参数 含义
--metrics <a,b,…> 必填。 逗号分隔的指标名
--group-by <items> 逗号分隔的 dataset.field 或 dataset.field:grain(粒度:day\|week\|month\|quarter\|year);metric_time[:grain] 表示按每个指标各自的主时间维度分组
--where <sql> 标量布尔 SQL,按 AND 子条件分别放置:引用所选指标的条件筛选结果行(在 --order / --limit 之前);窗口查询里只引用分组维度的条件也筛结果,名次和分区行数仍按全体计算;其余条件在聚合之前筛选输入行。见过滤条件。
--context-filter <sql> 行级 SQL,永远在聚合之前生效,决定窗口参与排名、计数的人群。要在子集内排名时用它。
--start-time <YYYY-MM-DD> 时间范围的下界,含该值
--end-time <YYYY-MM-DD> 上界,不含该值
--time-dimension <field> 该范围作用在哪个时间维度上(默认:group-by 里唯一的那个时间维度,否则是每个指标的主时间;metric_time 用于显式指明)
--order <keys> 逗号分隔的排序键;前缀 - 表示降序
--param <name=value[,value…]> 绑定一个 D-PARAM 参数(可重复)。值按指标声明的类型解析;逗号列表把指标展开成每个值一列;含逗号的字符串值请加引号。见 datus-extensions.md
--limit <n> 行数上限

有三条行为值得记牢(完整契约见 semantics.md):

  • 粒度的输出列命名。 --group-by orders.order_date:month 会产生名为 order_date__month 的列({field}__{grain}),--order 里也引用这个名字。
  • 排序键用输出列名,不是限定字段名。 写 --order -total_sales 或 --order ds__month,而不是 orders.status。前缀 - 表示降序; clap 会把它当成参数标志,所以 --order 特意允许了前导连字符。
  • 时间范围是左闭右开的 [start, end)。 --start-time 2024-01-01 --end-time 2025-01-01 包含整个 2024 年,且恰好不含 2025-01-01。 当既没有 --time-dimension、group-by 里也没有时间项时,范围会回退到每个指标的 主(聚合)时间维度(通过 Datus 的 D-TIME 扩展声明,或取数据集里唯一的 is_time 字段,见 datus-extensions.md); 只有当 group-by 里同时存在多个时间维度时才仍然需要显式的 --time-dimension (time_range_needs_dimension)。保留名 metric_time 用于显式选中主时间, 在 --group-by(metric_time:month → 输出列 metric_time__month)和 --time-dimension 里都可以用。

dosi select

编译一次明细查询:以某个数据集的粒度返回行级记录,而不是聚合后的指标。 同一个计划器、同一张关系图、同样的结构化错误 —— 完整指南见明细查询。

$ dosi select --model $DOSI_EXAMPLES/orders/model.yaml \
    --from orders \
    --fields orders.order_id,orders.amount,customers.name \
    --where "customers.tier = 'Enterprise'" \
    --order -amount --limit 20 --execute

它接受与 query 相同的 --dialect / --dws-mode / --pretty / --explain / --execute / --connection / --db,外加自己的 spec 参数:

参数 含义
--from <dataset> 必填。 结果粒度:一行输出对应该数据集的一行。不是 SQL 的 FROM ——其余数据集会自动连接。
--fields <a,b,...> 必填。 field、dataset.field,或 relationship[.relationship…].field;时间字段可加 :grain 后缀。
--where <sql> 行级过滤。可以引用投影里没有的数据集,引用到就会连上。聚合会被拒绝。
--start-time / --end-time 半开区间 [start, end)。
--time-dimension <field> 窗口作用在哪个时间字段上;默认取根数据集的主时间维度。
--order <k,-k,...> 排序键,必须是已投影的列(- 前缀表示降序)。
--limit <n> 默认 100,截到 10000。

三个值得记住的行为:

  • 输出列名去掉根前缀,其余用 __ 连接:orders.amount 是 amount, customers.name 是 customers__name。
  • 一对多方向的穿越会被拒绝,而不是悄悄产生重复行。问 --from customers --fields orders.order_id 会返回 detail_fanout, 并给出应该改用的根。
  • 两条关系路径都能抵达同一字段时,用关系前缀点名想要的那条 —— ambiguous_join_path 的 suggested_retry 会把两种写法直接打出来。 同样的前缀在 query --group-by 里也能用。

dosi explain

接受与 query 相同的 query spec 参数,但停在逻辑计划这一步,不生成 SQL。 适合在挑定方言之前,用来理解连接路径、扇出分支归属和粒度处理。

$ dosi explain --model $DOSI_EXAMPLES/tpcds/model.yaml \
    --metrics customer_lifetime_value --group-by store.s_state

计划以文本渲染,其中包含每个 JOIN 的类型(left / inner), 据此可以确认 Datus 的 join_type 扩展是否生效。 这里的 --format json 只作用于错误路径,成功的计划只有文本形式。

dosi attribute

把指标在两个 [start, end) 窗口之间的变化分解为各维度的贡献 —— 引擎自动执行所有需要的查询,并按指标类型选择精确的方法。返回内容与解读方式见 归因分析。

$ dosi attribute --model model.yaml \
    --metric revenue --dimensions status,customers.region \
    --baseline 2024-01-01..2024-02-01 --current 2024-02-01..2024-03-01 \
    --db warehouse.duckdb

窗口是 START..END 的 ISO 区间。--where 为整个分析加过滤; --connection/--db 选择数仓,与 query --execute 完全一致; --max-values、--top-dimensions、--top-values 约束输出规模。

dosi lineage

指标血缘图——物理表 → 数据集 → 原子指标 → 派生指标,加载了 ontology 时还有它的概念层—— 以 JSON 输出,或渲染成一个可以直接打开的页面。图里有什么见血缘。

$ dosi lineage dump --model model.yaml > graph.json
$ dosi lineage view --model model.yaml --ontology ontology.yaml

dump 打印图的 JSON(--out <path> 写到文件;契约本身就是 JSON,--format 对它不起作用)。 view 生成一个自包含的 HTML 页面并用默认浏览器打开;--out <path> 指定页面位置, --no-open——或者机器上没有浏览器——则改为打印文件路径,退出码仍为 0。 页面不需要网络,通过 file:// 就能打开。

两者都接受 --redact-sql:把每一段 SQL 文本和不透明的扩展负载替换成 "<redacted>", 图的形状不变,用于分享模型结构而不暴露表达式。--osi-basic 按严格 OSI 编译, 并在输出上标记 "mode": "basic"。 结果始终是 JSON。

输出格式

--format 接受三个值之一:

  • text(默认):面向人,结果行/列表是对齐的表格,编译结果是原始 SQL, 计划是缩进的树。NULL 单元格以灰显渲染。颜色会自动探测终端。
  • json:所有输出(以及所有错误)都是机器可读的。query 的编译结果是 {dialect, sql};--execute 会再加上 {columns, rows: [{col: val}], …}; validate 是 {issues, compile_errors};list 是一个对象数组。 错误带有稳定的 code、涉及到的名字、当一个错误引用存在备选时给出的 candidates,以及当某种改写能成功时给出的 suggested_retry, Agent 类调用方无需解析自然语言就能自我纠正。稳定 API 是错误码,不是错误文本。
  • arrow:在 stdout 上输出一个 Arrow IPC 流,仅用于 query --execute(其它命令会报错:--format arrow only applies to 'query --execute')。结果批次从数仓适配器直通 stdout,不做行物化, 可以零 JSON 解析地管进 DuckDB、Polars 或 pyarrow。需要支持 Arrow 的构建 (默认构建,或任意 exec-*-arrow / exec-flightsql / exec-duckdb feature; 一个不含这些 feature 的 --no-default-features 构建会在运行时拒绝 --format arrow)。
# Stream results into DuckDB for further analysis
dosi query --model model.yaml --metrics revenue --group-by orders.status \
    --execute --connection prod-ch --format arrow \
    | duckdb -c "SELECT * FROM read_arrow('/dev/stdin')"

# Or into Polars
dosi query ... --execute --format arrow \
    | python -c "import polars as pl,sys; print(pl.read_ipc_stream(sys.stdin.buffer))"

在数仓上执行

--execute 会执行编译出的 SQL 并打印结果行。不指定连接就用本地 DuckDB, 默认进程内、原生 Arrow(内置的 exec-duckdb);--db <file> 指向 DuckDB 文件,不指定则用内存库。带上 --connection <name>, 则指向该配置对应的数仓和方言。

$ dosi query --model model.yaml --metrics revenue --group-by orders.status \
    --execute --connection prod-sr

连接配置沿用 Datus agent.yml 的 datasources: 词汇表。可以把 --connections 指向一份完整的 agent.yml(读取 services.datasources),或一份独立的 datasources: 文件。不带该参数时, 按以下顺序发现该文件:DOSI_CONNECTIONS 环境变量 → ./dosi-connections.yaml → ./osi-connections.yaml → ~/.config/dosi/connections.yaml → ~/.config/osi/connections.yaml → ./conf/agent.yml → ~/.datus/conf/agent.yml。已有的 Datus 安装零配置就能用, 改名之前的 osi- 路径也仍然有效。密钥以 ${VAR} 的形式从环境变量插值。

datasources:
  prod-sr:
    type: starrocks
    host: sr.internal
    port: 9030
    arrow_flight_port: 9408      # opt into Arrow Flight SQL (SR ≥3.5.1)
    username: osi
    password: ${SR_PASSWORD}
    database: analytics
    default: true                # used by --execute without --connection
  prod-ch:
    type: clickhouse
    uri: http://ch.internal:8123
    username: default
    database: analytics

解析规则:

  • --execute 不带 --connection 时用标了 default: true 的那份配置; 一份都没标就退回本地 DuckDB(--db 或内存)。标了 default: true 却解析失败的配置(比如 ${VAR} 没设置)会在 stderr 上告警, 而不是悄悄用一个空的 DuckDB。
  • --dialect 与 --connection 同时使用时,方言必须与配置一致, 否则命令报错。去掉 --dialect,让配置来决定就行。

从源码构建时,数仓驱动是按 feature 开关的,所以自己编出来的可执行文件 只带上你点名要的那些(官方发布版则全都带上)。按需构建(或直接用 exec-all):

Feature 引擎 结果通路
exec-duckdb(默认) DuckDB(进程内) 原生 Arrow
exec-mysql MySQL、TiDB、StarRocks、Doris(MySQL 协议) 行
exec-postgres Postgres 行
exec-hologres Hologres(Postgres 协议;隐含 exec-postgres) 行
exec-dws 华为云 DWS(Postgres 协议;隐含 exec-postgres) 行
exec-gaussdb GaussDB / openGauss(原生 SHA256 认证驱动) 行
exec-oracle Oracle Database(ODPI-C;运行时需要 Instant Client) 行
exec-http ClickHouse、Trino 行
exec-http-arrow ClickHouse FORMAT ArrowStream 原生 Arrow
exec-flightsql StarRocks / Doris 的 Arrow Flight SQL(arrow_flight_port:) 原生 Arrow
exec-snowflake Snowflake(SQL API v2 + 密钥对 JWT) 行
exec-bigquery BigQuery(jobs.query REST + 服务账号 JWT) 行

各引擎的配置方式与 Arrow 结果通路的支持状态见 connectors.md 和 arrow.md。

另请参阅

  • semantics.md:CLI 所编译到的那份规范性行为契约 (指标推断、连接、扇出保护、时间处理)。
  • rest-api.md:同样的能力,通过 HTTP 提供。
  • extensions-guide.md:可选的 Datus 模型扩展 (join_type、fill_nulls_with、time_dimension、time_granularity、 time、dataset);它们的规范性契约与版本策略见 datus-extensions.md。
  • connectors.md:数仓连接器的配置。
  • arrow.md:如何启用 Arrow 结果传输,以及哪些环节会变快。