Skip to content

Errors

Every surface — CLI, REST, MCP, Python — reports failures with the same structured errors. Error codes are the stable API; message text is not: match on code (snake_case), read candidates / suggested_retry / hint to self-correct, and never parse prose.

How errors are structured

Errors are staged along the pipeline, one code enum per stage:

Stage When it runs Payload shape
load reading the model file text only today (no code)
validate structural checks on the model {severity, code, location, message}
compile lowering the model to IR {code, location, message, candidates?, hint?}
plan compiling a metric query {code, message, metrics?, candidates?, suggested_retry?}
execute running SQL on a warehouse {code, message, hint?}

Optional fields are omitted when empty, so payloads stay small. location says where to edit (metric 'revenue', semantic_model[0].relationships[2].to); candidates lists valid alternatives for a bad reference; suggested_retry / hint states the rewrite that would succeed.

Per surface:

  • CLI--format json prints the error struct as JSON on stderr; validate --format json prints {issues, compile_errors, warnings} on stdout. Exit codes: 0 success, 1 engine-rejected (structured error), 2 usage error. Known asymmetries: load errors and non-validate validation issues are text-only.
  • REST & MCP — the identical body, wrapped: {"error": {…}}. MCP tool failures return it as text content with isError: true; malformed MCP tool arguments return a plain-text schema error instead.
  • Python (dosi_engine) — ModelError / QueryError / ExecuteError, all with .code and .to_dict(); the planner's suggested_retry arrives as hint.
{"error": {"code": "unknown_metric",
           "message": "unknown metric \"revenu\"",
           "metrics": ["revenu"],
           "candidates": ["revenue", "order_count", "..."]}}

Where each code can occur

Surface validate issues compile errors plan errors execute errors
CLI validate ✓ (issues) ✓ (compile_errors)
CLI query / explain / list ✓ (text, aborts) query --execute only
REST POST /v1/validate, MCP validate_model ✓ (in body) ✓ (in body)
REST /v1/query/compile · /explain, MCP compile_sql · explain_query — (model loads at startup) ✓ (lazy compile)
REST /v1/query/execute, MCP run_query
Python Engine(...) ModelError("invalid_model") ModelError("compile_error")
Python compile_sql / query QueryError ExecuteError

Server-only codes (bad_request, unauthorized, forbidden, busy, internal, timeout) can accompany any REST/MCP call — see Server-level codes.

Model validation issues

Structural checks (dosi validate, POST /v1/validate). Errors make the model invalid; warnings don't — the model still compiles.

Code Severity When Fix
unsupported_version error version ≠ the OSI spec version this engine implements pin the version the message names
duplicate_name error two models, datasets, fields, metrics or relationships share a name rename one
empty_datasets error a semantic model has no datasets add at least one dataset
empty_columns error a relationship declares no column pairs fill from_columns / to_columns
column_count_mismatch error from_columns and to_columns differ in length they pair positionally — make them equal
unknown_dataset error a relationship endpoint names an undeclared dataset fix the from: / to: name
unknown_column error a relationship column is not a declared field of its dataset declare the field or fix the column
missing_sql_dialect error (metric) / warning (field) no ANSI_SQL (or other SQL) dialect entry add a SQL dialect expression
self_relationship warning a relationship joins a dataset to itself usually intentional; review
relationship_target_not_unique warning to_columns is not the target's primary/unique key — the join may fan out point the join at a unique key
{"severity": "error", "code": "unknown_dataset",
 "location": "semantic_model[0].relationships[0].to",
 "message": "relationship \"orders_to_customers\" references unknown dataset \"customers\""}

Compile errors

Lowering a valid model to IR (compile_errors in the validate envelope; on query surfaces they surface when the model first compiles). All carry location; candidates / hint where noted.

Code When Payload extras
unparseable_expression expression is not parseable SQL
no_sql_dialect a field/metric in use has no SQL dialect entry
unknown_dataset SUM(no_such.amount) — qualifier is not a dataset; also (mis)used when a document has ≠ 1 semantic models candidates: dataset names
unknown_field dataset.field where the dataset lacks that field candidates: that dataset's fields
unknown_column bare column found in no dataset hint: qualify as dataset.field
ambiguous_column bare column exists in several datasets candidates: qualified spellings
not_an_aggregate metric expression doesn't aggregate hint: wrap in SUM(...)
nested_aggregate SUM(MAX(...))
bare_column_in_metric non-aggregated column (or literal-only aggregate) in a metric hint: aggregate a dataset field
cross_dataset_aggregate one aggregate mixes columns of two datasets split the aggregate
count_star_needs_dataset bare COUNT(*) in a multi-dataset model hint: count a key column
unsupported_aggregate aggregate outside SUM/COUNT/COUNT DISTINCT/AVG/MIN/MAX
multi_column_count_distinct declared but currently never produced
window_in_metric window function in a metric expression hint: precompute in the dataset
subquery_in_metric subquery in a metric expression
measure_name_collision two different aggregates synthesize the same measure name hint on some sites
metric_reference_cycle D-DERIVE compose metrics reference each other message shows the cycle path
derive_expression_mismatch authored fallback expression ≠ the derive expansion hint
invalid_datus_extension malformed DATUS payload (bad v, unknown field ref, …) hint with a docs anchor
unsupported_datus_ext_version payload v has a newer major / pre-1.0 hint names the supported major
datus_ext_key_required requires: [...] names a key this engine doesn't implement
not_implemented a declared shape outside the current derive/window scope hint with a docs anchor

Compile warnings

Non-fatal: the model compiled, but not everything took effect. Shape {code, location, message}; CLI prints ! … on stderr and exits 0.

Code When
ignored_vendor_extension basic (standard-OSI) mode ignored a DATUS extension
datus_ext_version_ahead payload declares a newer minor; unknown keys were dropped (named in the message)
datus_ext_key_newer_than_declared a key newer than the declared v was honored anyway
compose_no_conformed_dimensions a compose metric's members share no dimension — no query can group it

Query planning errors

Rejections of a metric query (dosi query, /v1/query/*, compile_sql / run_query). All HTTP 400 except not_implemented501 and internal500.

Code When Payload extras
empty_query no metrics requested candidates: all metrics
unknown_metric metric name not in the model metrics, candidates
unknown_dimension group-by item not a dimension; also a window metric's time axis missing from the group-by candidates or suggested_retry
ambiguous_dimension bare field exists in several datasets candidates: qualified spellings
grain_on_non_time_dimension :grain on a non-time field suggested_retry
grain_too_fine requested grain finer than the stored time_granularity suggested_retry
duplicate_output_name two group-by items produce one output column suggested_retry
unknown_order_key order key is not an output column candidates: output columns
unsupported_filter subquery/window in where, or a malformed time range
aggregate_in_where aggregate in where (metric filters unsupported)
no_join_path no dataset reaches the measure and all group-bys many→one metrics
ambiguous_join_path more than one relationship path — semantics would differ candidates: the paths, suggested_retry
fan_out_risk duplicate-sensitive aggregate across a fanning join metrics, suggested_retry
time_range_needs_dimension several time dimensions in scope; range target unclear candidates
no_primary_time_dimension metric_time used but no primary time dimension declared candidates, suggested_retry
metric_time_conflict queried metrics disagree on their time axis metrics, suggested_retry
unconformed_dimension dimension outside a derived metric's conformed set metrics, candidates, suggested_retry
window_reset_too_fine window resets finer than the queried grain suggested_retry
dialect_unsupported_window_function target dialect lacks the needed window function metrics, suggested_retry
not_implemented legal per the model, outside current engine scope metrics, suggested_retry
internal engine invariant violation — retrying won't help; report a bug

A typical self-correction loop: unknown_metric → pick from candidates → retry; fan_out_risk → follow suggested_retry (group by reachable dimensions, or restate with COUNT DISTINCT/MIN/MAX):

{"code": "fan_out_risk",
 "message": "measure \"views_source_views_sum\" aggregates Sum over dataset \"views_source\", which has no many→one path to [\"bookings_source\"]; evaluating a duplicate-sensitive aggregate (SUM/AVG/COUNT) across that join would double-count rows",
 "metrics": ["views_times_booking_value"],
 "suggested_retry": "group by dimensions reachable from the measure's dataset, or restate the metric with a duplicate-insensitive aggregate (COUNT DISTINCT/MIN/MAX)"}

Execution errors

Running compiled SQL on a warehouse (query --execute, /v1/query/execute, run_query). Shape {code, message, hint?}.

Code HTTP When
config 400 connection profile broken (bad URI, unopenable DuckDB file, missing driver option)
connection 502 warehouse unreachable
auth 502 credentials rejected
sql_rejected 502 the warehouse rejected the SQL (missing table, type error) — message embeds the warehouse's own error
driver 502 driver-level failure after connecting
timeout 504 statement or server execute timeout

Server-level codes

dosi-server adds transport-level codes in the same envelope, {"error": {"code", "message"}}:

Code HTTP When
bad_request 400 malformed JSON body, unknown dialect
unauthorized 401 missing/wrong bearer token
forbidden 403 execute called with --disable-execute
busy 429 all execution slots taken (Retry-After: 1)
timeout 504 the server's own execute timeout
internal 500 unexpected server failure

Python exceptions

The dosi_engine wheel raises one exception hierarchy (OsiError base, .code + .to_dict() on all):

Exception Codes Notes
ModelError io, parse, invalid_model, compile_error invalid_model carries issues; compile_error carries compile_errors plus the first error's candidates/hint
QueryError every plan code above suggested_retry arrives as hint
ExecuteError every execute code above

Compile warnings are raised as Python RuntimeWarnings at Engine(...) construction — python -W error promotes them to failures.

SQL-generation errors (parse_error, unsupported_aggregate, …) are an internal boundary between planner and renderer; they surface inside compile or plan errors above, never as a user-level contract of their own.

Every code on this page is pinned by tests: the negative-case catalog in fixtures/errors/ runs on every commit, and its actual payloads are browsable on the test-status dashboard ("Error contract" section).