Query Language¶
OrionBelt uses a structured query object to express analytical queries against a semantic model. The query language selects dimensions and measures, applies filters, sorts results, and limits output — all using business names rather than raw SQL.
Query Object Structure¶
select:
dimensions:
- Customer Country
- "Order Date:month" # with time grain
measures:
- Revenue
- Order Count
where:
- field: Customer Segment
op: in
value: [SMB, MidMarket]
having:
- field: Revenue
op: gt
value: 10000
order_by:
- field: Revenue
direction: desc
limit: 1000
In Python¶
from orionbelt.models.query import (
QueryObject,
QuerySelect,
QueryFilter,
QueryOrderBy,
FilterOperator,
SortDirection,
)
query = QueryObject(
select=QuerySelect(
dimensions=["Customer Country", "Order Date:month"],
measures=["Revenue", "Order Count"],
),
where=[
QueryFilter(field="Customer Segment", op=FilterOperator.IN, value=["SMB", "MidMarket"]),
],
having=[
QueryFilter(field="Revenue", op=FilterOperator.GT, value=10000),
],
order_by=[QueryOrderBy(field="Revenue", direction=SortDirection.DESC)],
limit=1000,
)
Select¶
The select section specifies which dimensions and measures to include.
Dimensions¶
Dimensions are referenced by name as defined in the semantic model. They become GROUP BY columns in the generated SQL.
Time Grain Override¶
Apply a time grain at query time using "dimension:grain" syntax:
select:
dimensions:
- "Order Date:month" # truncate to month
- "Order Date:quarter" # truncate to quarter
- "Order Date:year" # truncate to year
Supported grains: year, quarter, month, week, day, hour, minute, second.
This overrides any timeGrain set on the dimension definition.
Measures¶
Measures are referenced by name. They can be simple aggregations, expression-based measures, or metrics.
Secondary Join Paths¶
When a model defines secondary joins (e.g., Flights → Airports via departure and arrival), use usePathNames to select which join path to use:
select:
dimensions:
- Airport Name
measures:
- Total Ticket Price
usePathNames:
- source: Flights
target: Airports
pathName: arrival
Each entry specifies a (source, target, pathName) triple. The pathName must match a secondary join defined in the model. When active, the secondary join replaces the primary join for that pair.
In Python¶
from orionbelt.models.query import QueryObject, QuerySelect, UsePathName
query = QueryObject(
select=QuerySelect(
dimensions=["Airport Name"],
measures=["Total Ticket Price"],
),
use_path_names=[
UsePathName(source="Flights", target="Airports", path_name="arrival"),
],
)
In JSON (full mode)¶
{
"select": {
"dimensions": ["Airport Name"],
"measures": ["Total Ticket Price"]
},
"usePathNames": [
{"source": "Flights", "target": "Airports", "pathName": "arrival"}
]
}
If a usePathNames entry references a non-existent data object or pathName, the query will return a resolution error.
Dimension Exclusion (Anti-Join)¶
The dimensionsExclude flag inverts a dimension-only query to return value combinations that do not exist in the data. This is useful for finding missing relationships — for example, directors and producers who have never collaborated on a movie.
This generates an anti-join query using SQL EXCEPT:
- All possible combinations — A
CROSS JOINof the distinct values of each dimension - Existing combinations — The actual dimension pairs found through the join graph
- Result — All combinations
EXCEPTexisting ones
Constraints¶
- No measures allowed —
dimensionsExcludeonly works with dimension-only queries (no measures or metrics) - 2+ dimensions required — At least two dimensions must be specified
In Python¶
from orionbelt.models.query import QueryObject, QuerySelect
query = QueryObject(
select=QuerySelect(dimensions=["Director", "Producer"]),
dimensions_exclude=True,
)
In JSON¶
Error Codes¶
| Error Code | Cause |
|---|---|
DIMENSIONS_EXCLUDE_WITH_MEASURES |
Query includes measures — not allowed with dimensionsExclude |
DIMENSIONS_EXCLUDE_INSUFFICIENT |
Fewer than 2 dimensions specified |
Filters¶
Filters restrict the result set. Dimension filters go in where (become SQL WHERE), and measure filters go in having (become SQL HAVING).
where:
# By dimension name
- field: Customer Country
op: equals
value: Germany
# By qualified column (DataObject.Column) — no dimension needed
- field: Orders.Order Status
op: equals
value: F
having:
- field: Revenue
op: gte
value: 5000
Multiple top-level filters are combined with AND. For OR logic or more complex boolean expressions, use filter groups.
Filter Structure¶
| Property | Type | Description |
|---|---|---|
field |
string | Dimension name or DataObject.Column (where), measure name (having) |
op |
string | Filter operator (see table below) |
value |
any | Comparison value (string, number, list, etc.) |
Filter Groups (AND / OR / NOT)¶
A filter group combines multiple filters with and or or logic. Groups can be nested recursively for complex boolean expressions.
where:
# Simple OR: country is US or CA
- logic: or
filters:
- field: Customer Country
op: equals
value: US
- field: Customer Country
op: equals
value: CA
Nested groups: (A OR B) AND C¶
where:
- logic: and
filters:
- logic: or
filters:
- field: Customer Country
op: equals
value: US
- field: Customer Country
op: equals
value: CA
- field: Market Segment
op: equals
value: BUILDING
Negation: NOT (A OR B)¶
where:
- logic: or
negated: true
filters:
- field: Order Status
op: equals
value: P
- field: Order Status
op: equals
value: F
Filter Group Properties¶
| Property | Type | Default | Description |
|---|---|---|---|
logic |
enum | and |
and or or — how to combine child filters |
filters |
list | — | Child filters (leaf filters or nested filter groups) |
negated |
bool | false |
Wrap the combined expression with NOT |
Filter groups and leaf filters can be mixed freely at any level — in where, having, or nested inside other groups.
In Python¶
from orionbelt.models.query import (
QueryFilter, QueryFilterGroup, FilterOperator,
)
# (country = 'US' OR country = 'CA') AND segment = 'BUILDING'
where = [
QueryFilterGroup(
logic="and",
filters=[
QueryFilterGroup(
logic="or",
filters=[
QueryFilter(field="Customer Country", op=FilterOperator.EQ, value="US"),
QueryFilter(field="Customer Country", op=FilterOperator.EQ, value="CA"),
],
),
QueryFilter(field="Market Segment", op=FilterOperator.EQ, value="BUILDING"),
],
),
]
Filter Reachability¶
A where filter field can reference:
- A dimension name (e.g.
Order Priority) — resolves to the dimension's data object and column - A qualified column using
DataObject.Columndot notation (e.g.Orders.Order Priority) — directly references a column without requiring a dimension definition
The referenced data object must be reachable from the query's join graph:
- Directly joined in the query (base object or any object in the join path)
- A descendant — reachable via directed joins from any already-joined object
If the data object is reachable but not yet in the join path, it is auto-joined automatically. If the data object is not reachable at all, the query returns an UNREACHABLE_FILTER_FIELD error.
A having filter field must reference a measure name.
Filter Operators¶
OrionBelt supports two operator naming conventions — OBML style and SQL style. Both are equivalent.
Comparison Operators¶
| OBML | SQL Style | SQL Output | Value Type |
|---|---|---|---|
equals |
=, eq |
= value |
scalar |
notequals |
!=, neq |
<> value |
scalar |
gt |
>, greater |
> value |
scalar |
gte |
>=, greater_eq |
>= value |
scalar |
lt |
<, less |
< value |
scalar |
lte |
<=, less_eq |
<= value |
scalar |
Set Operators¶
| OBML | SQL Style | SQL Output | Value Type |
|---|---|---|---|
inlist |
in |
IN (v1, v2, ...) |
list |
notinlist |
not_in |
NOT IN (v1, v2, ...) |
list |
Null Operators¶
| OBML | SQL Style | SQL Output | Value Type |
|---|---|---|---|
set |
is_not_null |
IS NOT NULL |
none |
notset |
is_null |
IS NULL |
none |
String Operators¶
| Operator | SQL Output | Value Type |
|---|---|---|
contains |
LIKE '%value%' (dialect-specific) |
string |
notcontains |
NOT LIKE '%value%' |
string |
starts_with |
LIKE 'value%' |
string |
ends_with |
LIKE '%value' |
string |
like |
LIKE 'pattern' |
string |
notlike |
NOT LIKE 'pattern' |
string |
Range Operators¶
| Operator | SQL Output | Value Type |
|---|---|---|
between |
BETWEEN low AND high |
list of 2 |
notbetween |
NOT BETWEEN low AND high |
list of 2 |
relative |
Relative time range | object |
Relative filter object
The relative operator expects an object with the following keys:
unit: one ofday,week,month,yearcount: positive integer number of unitsdirection(optional):past(default) orfutureinclude_current(optional): boolean, defaulttrue
Example (last 7 days, inclusive of today):
where:
- field: Order Date
op: relative
value:
unit: day
count: 7
direction: past
include_current: true
String contains is dialect-aware
The contains operator generates different SQL per dialect:
- Postgres/ClickHouse:
ILIKE '%' || value || '%' - Snowflake:
CONTAINS(column, value) - Dremio/Databricks:
LOWER(column) LIKE '%' || LOWER(value) || '%'
Ordering¶
Sort results by dimension or measure names from the query's SELECT, or by numeric position:
order_by:
- field: Revenue
direction: desc
- field: Customer Country
direction: asc # default
- field: "1" # numeric position (1-based)
direction: asc
| Property | Type | Default | Description |
|---|---|---|---|
field |
string | — | Dimension or measure name from the query's SELECT, or a numeric position (e.g. "1") |
direction |
enum | asc |
asc or desc |
The field must reference a dimension or measure that appears in the query's select. It cannot reference fields outside the SELECT. Alternatively, use a numeric string to reference the SELECT column by position (1-based).
Limit¶
Restrict the number of returned rows:
Validation¶
Invalid queries return error responses:
| Error Code | Status | Cause |
|---|---|---|
UNKNOWN_DIMENSION |
400 | Dimension name not in model |
UNKNOWN_MEASURE |
400 | Measure name not in model |
UNKNOWN_FILTER_FIELD |
400 | Filter field is not a dimension (WHERE) or measure (HAVING) |
UNREACHABLE_FILTER_FIELD |
400 | Filter dimension's data object is not reachable from join graph |
UNKNOWN_ORDER_BY_FIELD |
400 | ORDER BY field not in query's SELECT |
INVALID_ORDER_BY_POSITION |
400 | Numeric ORDER BY position out of range |
INVALID_FILTER_OPERATOR |
400 | Unrecognized filter operator |
INVALID_RELATIVE_FILTER |
400 | Malformed relative time filter |
AMBIGUOUS_JOIN |
422 | Multiple join paths possible |
DIMENSIONS_EXCLUDE_WITH_MEASURES |
400 | dimensionsExclude used with measures |
DIMENSIONS_EXCLUDE_INSUFFICIENT |
400 | dimensionsExclude with fewer than 2 dimensions |
Semantics Summary¶
| Query Element | SQL Equivalent |
|---|---|
select.dimensions |
SELECT + GROUP BY columns |
select.measures |
SELECT aggregate expressions |
where |
WHERE clause |
having |
HAVING clause |
order_by |
ORDER BY clause |
limit |
LIMIT clause |
dimensionsExclude |
Anti-join via CROSS JOIN + EXCEPT (dimension-only) |