Filters
Use typed filter helpers in dataset queries and filtered measures.
Filters are structured query objects. They are used by dataset queries and filtered measures.
import {
between,
eq,
gt,
gte,
inList,
isNotNull,
isNull,
like,
lt,
lte,
neq,
notInList,
} from '@hypequery/datasets';Filter helpers
eq('status', 'completed')
neq('status', 'cancelled')
gt('amount', 100)
gte('amount', 100)
lt('amount', 1000)
lte('amount', 1000)
inList('country', ['US', 'UK', 'CA'])
notInList('status', ['cancelled', 'refunded'])
between('createdAt', '2026-01-01', '2026-01-31')
like('email', '%@example.com')
isNull('churnedAt')
isNotNull('churnedAt')Each helper returns a filter with a field, operator, and value. isNull and isNotNull take no value: they render IS NULL and IS NOT NULL, and a filter that sets value on them is rejected.
eq('status', 'completed')
// { field: 'status', operator: 'eq', value: 'completed' }Timestamp values
Timestamp filters take ISO 8601 strings. A date ('2026-02-01') or an
offset-free date-time ('2026-02-01T00:00:00', '2026-02-01 00:00:00') is
read as local time in the query timezone. A value carrying Z or an offset,
such as new Date().toISOString() produces, is an instant and selects the
same rows in every query timezone:
gte('createdAt', '2026-02-01T00:00:00Z') // instant
gte('createdAt', '2026-02-01T00:00:00.000Z') // instant, fractional seconds kept
gte('createdAt', '2026-02-01') // local midnight in the query timezoneA between or notIn may mix the two forms; each bound keeps its own
meaning. An in list must use one form or the other.
Use filters in dataset queries
await analytics.execute(Orders, {
dimensions: ['country', 'status'],
measures: ['revenue', 'orderCount'],
filters: [
eq('status', 'completed'),
between('createdAt', '2026-01-01', '2026-01-31'),
],
});Filter aggregated values with having
filters narrow the rows before they are aggregated. To keep only the groups whose aggregated value passes a condition, such as countries with more than 10,000 in revenue, use having:
await analytics.execute(Orders, {
dimensions: ['country'],
measures: ['revenue', 'orderCount'],
filters: [eq('status', 'completed')],
having: [
{ measure: 'revenue', operator: 'gt', value: 10_000 },
{ measure: 'orderCount', operator: 'between', value: [2, 50] },
],
});- Each condition must name a measure the query selects, including derived measures. A condition on an unselected measure is rejected.
- Values are finite numbers, bound as query parameters. Operators are
eq,neq,gt,gte,lt,lte,between(a[min, max]pair),inandnotIn(non-empty arrays). - Conditions apply after grouping and before ordering and limits. A group whose measure is
NULLnever matches. - A dataset's
limits.maxFiltersboundsfiltersandhavingtogether. - Queries that select window or shift measures do not accept
having.
Use filters in measures
Filtered measures are useful when a business state should always be part of the aggregation.
import { dataset, dimension, eq, measure } from '@hypequery/datasets';
const Orders = dataset('orders', {
source: 'orders',
dimensions: {
status: dimension.string(),
},
measures: {
completedRevenue: measure.sum('amount', {
filters: [eq('status', 'completed')],
}),
refundedRevenue: measure.sum('amount', {
filters: [eq('status', 'refunded')],
}),
},
});Restrict filter operators
By default, filterable dimensions become available as filters. Use the dataset filters map when you want a named filter contract or a restricted operator set.
import { dataset, dimension, measure } from '@hypequery/datasets';
const Orders = dataset('orders', {
source: 'orders',
dimensions: {
status: dimension.string(),
country: dimension.string(),
},
measures: {
revenue: measure.sum('amount'),
},
filters: {
status: {
__type: 'filter_definition',
field: 'status',
operators: ['eq', 'neq', 'in', 'notIn'],
},
},
});With this contract, status allows only the listed operators, and country is still filterable with the default operators. Declared filters add to the dimensions marked filterable; they do not replace them. TypeScript checks these names and operators in dataset and metric execution, Serve API inputs, and React hooks, including infinite-query hooks. The filter helpers preserve their operator literals, so eq('status', 'completed') works with an eq-only policy.
Use DatasetQueryFor<typeof Orders> or MetricQueryFor<typeof Orders, 'revenue'> when storing query inputs in variables. DatasetFilterNames<typeof Orders> describes the exposed names, and DatasetFilterFor<typeof Orders> describes the field/operator pairs. A declared filter's name is its key in filters, which may differ from its backing dimension. A dimension named directly takes the operators of the filters declared on it, so declaring planFilter on plan with operators: ['eq'] also limits plan to eq.
isNull and isNotNull are opt-in for query filters: a filter allows them only when its operators list names them, as in operators: ['gte', 'isNull', 'isNotNull']. Measure filters and segments can use them without a list.
Every dimension with filterable !== false is filterable, whether or not filters is declared; set filterable: false to remove one, or declare a filter to expose a filterable: false dimension under its own policy. One-hop belongsTo and hasOne filters reach the target's filterable dimensions and the filters it declares under a dimension's name, with the target's operator policy. SQL-backed target dimensions, hasMany, and multi-hop paths remain unavailable.
Keep dataset definitions inferred to retain their literal policies. Broad DatasetInstance or SemanticFiltersDefinition annotations lose that information. Explicit result-row generic calls retain the legacy dynamic-query contract. Filter values and runtime tenant restrictions continue to be validated when the query runs.
Order helpers
asc and desc create structured order clauses.
import { asc, desc } from '@hypequery/datasets';
await analytics.execute(Orders, {
dimensions: ['country'],
measures: ['revenue'],
orderBy: [desc('revenue'), asc('country')],
});Tenant filters
When runtime tenancy is active, explicit filters on the tenant field are rejected. Use runtime tenant context instead.
await analytics.execute(Orders, {
measures: ['revenue'],
filters: [eq('tenantId', 'tenant_123')],
}, {
runtime: {
tenant: 'tenant_123',
},
});
// Error: Cannot filter on tenant field "tenantId" when runtime tenancy enforcement is active.