Skip to content

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:

$ cargo install dosi-engine

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.

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

--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:month produces a column named order_date__month ({field}__{grain}). This is also what you reference in --order.
  • Order keys are output column names, not qualified fields. --order -total_sales or --order ds__month — never orders.status. The - prefix descends; because clap would read it as a flag, --order allows leading hyphens.
  • Time ranges are half-open [start, end). --start-time 2024-01-01 --end-time 2025-01-01 includes all of 2024 and excludes 2025-01-01 exactly. With no --time-dimension and 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 single is_time field — see datus-extensions.md); only a group-by holding several time dimensions still needs an explicit --time-dimension (time_range_needs_dimension). The reserved name metric_time selects the primary time explicitly, in --group-by (metric_time:month → output column metric_time__month) and --time-dimension alike.

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. NULL cells render dimmed. Colors auto-detect the terminal.
  • json — every output (and every error) is machine-readable. A query compile is {dialect, sql}; --execute adds {columns, rows: [{col: val}], …}; validate is {issues, compile_errors}; list is an array of objects. Errors carry a stable code, the names involved, candidates where a bad reference has alternatives, and a suggested_retry where 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, for query --execute only (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 any exec-*-arrow / exec-flightsql / exec-duckdb feature; a --no-default-features build without one rejects --format arrow at 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:

  • --execute without --connection uses the profile marked default: true; if none is marked, it falls back to local DuckDB (--db or in-memory). A default: true entry that failed to parse (e.g. an unset ${VAR}) warns on stderr rather than silently using empty DuckDB.
  • --dialect with --connection must agree with the profile's dialect, or the command errors — drop --dialect and 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.