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,
先导出一次,下面的命令就都能直接复制运行:
SINCE 是引入该键的 datus-ext 版本,IF IGNORED 是不采纳它要付出的代价,
见 datus-extensions.md §2.1。
--format json 以机器契约的形式输出同样的内容。
生成模型的 Agent 在决定输出哪些键之前,应该先读它。
dosi validate¶
加载模型并跑完整的校验流水线:结构、名称唯一性、关系引用完整性、 指标表达式编译,然后报告每一个问题。
--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 会拒绝本版本接受的所有模型。
$ 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-duckdbfeature; 一个不含这些 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 结果传输,以及哪些环节会变快。