CLI reference¶
dosi (crate crates/dosi-engine) is the command-line
interface to dosi-engine: validate an OSI semantic model, browse its compiled
metrics/datasets/dimensions, compile a metric query to dialect SQL, and
execute that SQL against a warehouse. Everything the
REST API does over HTTP, dosi does from a shell — same
compiler, same structured errors, same --format json machine contract.
$ 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
explain Show the compiled IR shape and logical plan without generating SQL
Installing¶
dosi installs as a single self-contained binary — the workspace library
crates and, by default, DuckDB are statically linked into one executable, so
there is nothing else to install and no separate packages to manage. It lands
in ~/.cargo/bin/osi (on PATH).
Requirements: a Rust toolchain (≥ the workspace rust-version) and a C++
compiler (bundled DuckDB compiles from source). TLS in the optional connectors
is rustls — no OpenSSL or other system libraries. Use --no-default-features
to drop bundled DuckDB (then a duckdb CLI is needed at runtime) and with it
the C++ build requirement.
From a git repository¶
Works against a private repo too — any user with read access (SSH key or a git credential helper) can install directly; no crates.io publish required:
$ cargo install --git ssh://git@github.com/datus-ai/osi-engine.git dosi-engine
# if SSH fetch fails, let cargo use your system git:
$ CARGO_NET_GIT_FETCH_WITH_CLI=true \
cargo install --git ssh://git@github.com/datus-ai/osi-engine.git dosi-engine
Pin a version with --tag <tag> / --branch <branch> / --rev <sha>, add
--locked to build against the committed Cargo.lock, and re-run with
--force to update. Because each user compiles locally, the binary matches
their own OS/CPU (Linux, Intel or Apple-Silicon macOS) automatically.
From crates.io¶
Once published:
With warehouse connectors¶
Connectors are cargo features (see Executing against a warehouse); pass them at install time:
$ cargo install --git <url> dosi-engine --features exec-all # every connector
$ cargo install --git <url> dosi-engine --features exec-mysql,exec-postgres
$ cargo install --git <url> dosi-engine --no-default-features # lean, no bundled DuckDB
Building from source¶
From a checkout, scripts/build_binary.sh builds
a release + stripped binary and reports its size and self-containment:
$ scripts/build_binary.sh # default: bundled DuckDB, stripped
$ scripts/build_binary.sh --all # + every connector (exec-all)
$ scripts/build_binary.sh --lean # no bundled DuckDB
Or directly: cargo build --release -p dosi-engine --bin osi (the binary is
target/release/dosi). The result is dynamically linked only against standard
system libraries (glibc ≥ 2.34, libstdc++); DuckDB itself is statically bundled.
Global options¶
These apply to every command (clap global = true):
| Flag | Env | Default | Meaning |
|---|---|---|---|
--model <path> |
DOSI_MODEL |
required | OSI model file (.yaml / .yml / .json) |
--format <fmt> |
— | text |
Output format: text, json, or arrow (see Output formats) |
--connections <path> |
DOSI_CONNECTIONS |
discovery chain | Connections file for --execute (see Executing against a warehouse) |
--osi-datus |
— | on | Datus mode: DATUS custom_extensions are honored (datus-extensions.md) |
--osi-basic |
— | off | Basic mode: strict standard OSI — DATUS extensions ignored with a warning; validate also runs the upstream validator (see below) |
--osi-datus and --osi-basic are mutually exclusive; passing both is a
usage error. In basic mode every ignored extension prints a ! warning line
on stderr (and lands in warnings under --format json); warnings never
change the exit code.
--model may be omitted if DOSI_MODEL is set; a command that needs a model
and finds neither exits with no model given; pass --model <path> or set
DOSI_MODEL.
Exit codes: 0 success, 1 a query/execute the engine rejected (with a
structured error), 2 a usage/CLI error (bad flag, missing model, unparseable
argument).
Commands¶
dosi info¶
Reports the engine, OSI-spec and datus-ext versions, plus every
vendor_name: DATUS custom_extensions key this engine reads. Needs no model,
so it doubles as a version probe.
$ dosi info
dosi 0.1.0
osi spec 0.2.0.dev0
datus-ext 1.1 (accepts 1.0 and up)
mode datus
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
dataset D-DATASET metric 1.1 degraded
SINCE is the datus-ext version that introduced the key; IF IGNORED is what
it costs a consumer that does not honor it — see
datus-extensions.md §2.1.
--format json emits the same content as a machine contract, which is what a
model-generating agent should read before choosing which keys to emit.
dosi validate¶
Loads the model and runs the full validation pipeline — structure, unique names, relationship reference integrity, and metric-expression compilation — then reports every issue.
--format json emits {issues, compile_errors, warnings} for CI gating: each
issue carries a severity and message, each compile error a stable code and
optional hint, each warning a stable code, location, and message. A
model with errors exits non-zero; warnings alone do not.
Upstream validation (--osi-basic). In basic mode validate additionally
shells out to the upstream apache/ossie
reference validator when a checkout is found — OSSIE_DIR (default
~/src/ossie) containing validation/validate.py — holding the model to the
published spec. An upstream failure fails the command; a missing checkout or
missing python3 prints a note and falls back to built-in validation only.
--format json adds "upstream": {ran, passed, output, note}.
$ OSSIE_DIR=~/src/apache-ossie dosi validate --osi-basic --model model.yaml
✓ upstream OSI validation passed
✓ 1 semantic model(s) valid
dosi list <what>¶
Browse the compiled semantic layer. Three subcommands, each a table in
text mode or an array of objects under --format json:
| Subcommand | Columns |
|---|---|
dosi list datasets |
name, source, primary key, field count, time dimensions |
dosi list metrics |
name, inferred kind (aggregate / ratio / expression), datasets, description |
dosi list dimensions |
dataset.field, time flag, description |
$ dosi list metrics --model fixtures/tpcds/model.yaml
NAME KIND DATASETS DESCRIPTION
total_sales aggregate store_sales Total sales revenue across all transactions
customer_lifetime_value ratio customer, store_sales Average lifetime sales value per customer
store_productivity expression store, store_sales Sales per employee across stores
dosi query¶
The core command: compile a metric query into dialect SQL, and optionally run
it. The query is described by the shared query spec flags below; on top of
those, query takes:
| Flag | Meaning |
|---|---|
--dialect <name> |
Target SQL dialect (default duckdb, or the --connection profile's dialect) |
--pretty |
Pretty-print the generated SQL |
--explain |
Also print the logical plan above the SQL |
--execute |
Run the compiled SQL against a warehouse and print the rows |
--connection <name> |
Named profile from the connections file (implies its dialect) |
--db <path> |
DuckDB file for --execute without --connection (default: in-memory) |
Compile only (no --execute) prints the SQL (text) or {dialect, sql}
(json):
$ dosi query --model fixtures/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, ...
(Combining total_sales with a store-based metric like store_productivity
here would be a fan_out_risk error — a per-store measure can't be grouped by
a date the store doesn't reach. That protection is the point; see
semantics.md §6.)
Query spec flags (shared with explain)¶
| Flag | Meaning |
|---|---|
--metrics <a,b,…> |
Required. Comma-separated metric names |
--group-by <items> |
Comma-separated dataset.field or dataset.field:grain (grain: day\|week\|month\|quarter\|year); metric_time[:grain] groups by each metric's primary time dimension |
--where <sql> |
Scalar boolean SQL over dimension fields, applied before aggregation |
--start-time <YYYY-MM-DD> |
Inclusive lower bound of a time range |
--end-time <YYYY-MM-DD> |
Exclusive upper bound |
--time-dimension <field> |
Which time dimension the range applies to (default: the only time dimension in the group-by, else each metric's primary time; metric_time says so explicitly) |
--order <keys> |
Comma-separated order keys; a - prefix means descending |
--limit <n> |
Row limit |
Three behaviors worth internalizing (the full contract is in semantics.md):
- Grain output naming.
--group-by orders.order_date:monthproduces a column namedorder_date__month({field}__{grain}). This is also what you reference in--order. - Order keys are output column names, not qualified fields.
--order -total_salesor--order ds__month— neverorders.status. The-prefix descends; because clap would read it as a flag,--orderallows leading hyphens. - Time ranges are half-open
[start, end).--start-time 2024-01-01 --end-time 2025-01-01includes all of 2024 and excludes 2025-01-01 exactly. With no--time-dimensionand no time item in the group-by, the range falls back to each metric's primary time dimension (declared via the Datus D-TIME extension, or the dataset's singleis_timefield — see datus-extensions.md); only a group-by holding several time dimensions still needs an explicit--time-dimension(time_range_needs_dimension). The reserved namemetric_timeselects the primary time explicitly, in--group-by(metric_time:month→ output columnmetric_time__month) and--time-dimensionalike.
dosi explain¶
Takes the same query spec flags as query but stops at the logical plan — no
SQL is generated. Useful for understanding join paths, fan-out branch
assignment, and grain handling before you pick a dialect.
$ dosi explain --model fixtures/tpcds/model.yaml \
--metrics customer_lifetime_value --group-by store.s_state
The plan renders as text (including each join's kind, left / inner, so you
can confirm a Datus join_type extension took effect).
--format json applies only to the error path here — a successful plan is
text-only.
Output formats¶
--format takes one of three values:
text(default) — human-readable: an aligned table for rows/listings, the raw SQL for a compile, an indented tree for a plan.NULLcells render dimmed. Colors auto-detect the terminal.json— every output (and every error) is machine-readable. Aquerycompile is{dialect, sql};--executeadds{columns, rows: [{col: val}], …};validateis{issues, compile_errors};listis an array of objects. Errors carry a stablecode, the names involved,candidateswhere a bad reference has alternatives, and asuggested_retrywhere a rewrite would succeed — so agentic callers self-correct without parsing prose. Error codes are the stable API; error text is not.arrow— an Arrow IPC stream on stdout, forquery --executeonly (any other command errors:--format arrow only applies to 'query --execute'). Result batches pass straight from the warehouse adapter to stdout with no row materialization — pipe them into DuckDB, Polars, or pyarrow with zero JSON parsing. Requires an Arrow-capable build (the default build, or anyexec-*-arrow/exec-flightsql/exec-duckdbfeature; a--no-default-featuresbuild without one rejects--format arrowat runtime).
# 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))"
Executing against a warehouse¶
--execute runs the compiled SQL and prints the rows. Without a connection it
uses local DuckDB — in-process and Arrow-native by default (bundled
exec-duckdb); --db <file> points at a DuckDB file, otherwise it is
in-memory. With --connection <name> it targets that profile's warehouse and
dialect.
$ dosi query --model model.yaml --metrics revenue --group-by orders.status \
--execute --connection prod-sr
Connection profiles use the
Datus agent.yml datasources:
vocabulary. Point --connections at a full agent.yml (reads
services.datasources) or a standalone datasources: file. Without the flag,
the file is discovered in order: DOSI_CONNECTIONS env →
./dosi-connections.yaml → ./osi-connections.yaml →
~/.config/dosi/connections.yaml → ~/.config/osi/connections.yaml →
./conf/agent.yml → ~/.datus/conf/agent.yml — an existing Datus install
works with zero config, and the pre-rename osi- paths still resolve.
Secrets interpolate from the environment as ${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
Resolution rules:
--executewithout--connectionuses the profile markeddefault: true; if none is marked, it falls back to local DuckDB (--dbor in-memory). Adefault: trueentry that failed to parse (e.g. an unset${VAR}) warns on stderr rather than silently using empty DuckDB.--dialectwith--connectionmust agree with the profile's dialect, or the command errors — drop--dialectand let the profile decide.
Warehouse drivers are feature-gated so the default binary stays lean.
Build with the features you need (or exec-all):
| Feature | Engines | Result path |
|---|---|---|
exec-duckdb (default) |
DuckDB (in-process) | Arrow-native |
exec-mysql |
MySQL, TiDB, StarRocks, Doris (MySQL wire) | rows |
exec-postgres |
Postgres | rows |
exec-hologres |
Hologres (Postgres wire; implies exec-postgres) |
rows |
exec-gaussdb |
GaussDB / openGauss (native SHA256 auth driver) | rows |
exec-oracle |
Oracle Database (ODPI-C; Instant Client at runtime) | rows |
exec-http |
ClickHouse, Trino | rows |
exec-http-arrow |
ClickHouse FORMAT ArrowStream |
Arrow-native |
exec-flightsql |
StarRocks / Doris Arrow Flight SQL (arrow_flight_port:) |
Arrow-native |
exec-snowflake |
Snowflake (SQL API v2 + key-pair JWT) | rows |
Per-engine setup and the Arrow result-path status live in connectors.md and arrow.md.
See also¶
- semantics.md — the normative behavior contract (metric inference, joins, fan-out protection, time handling) the CLI compiles to.
- rest-api.md — the same capabilities over HTTP.
- extensions-guide.md — the optional Datus model
extensions (
join_type,fill_nulls_with,time_dimension,time_granularity,dataset), and datus-extensions.md for their normative contract and versioning. - connectors.md — warehouse connector setup.
- arrow.md — enabling Arrow result transfer and what gets faster.