Definition
Create typed dataset definitions over ClickHouse tables and views.
A dataset is a typed semantic model over a source table or view.
import { dataset, dimension, measure } from '@hypequery/datasets';
const Orders = dataset('orders', {
source: 'orders',
tenantKey: 'tenant_id',
timeKey: 'created_at',
dimensions: {
id: dimension.number(),
createdAt: dimension.timestamp({ column: 'created_at' }),
},
measures: {
revenue: measure.sum('amount'),
},
});source is the physical table or view name. The first dataset() argument is the logical dataset name. tenantKey and timeKey are physical column names used for runtime tenant isolation and time graining.
Versioned tables (rowResolution)
ReplacingMergeTree and CollapsingMergeTree tables keep several rows per key until a background merge runs, and merges never finish on a schedule you control. Queried directly, a subscription with three status changes counts three times. Set rowResolution to read the source with FINAL, so every query sees one row per sorting key:
// ReplacingMergeTree(updated_at): one row per subscription status change
export const Subscriptions = dataset('subscriptions', {
source: 'saas.subscriptions',
rowResolution: { kind: 'final' },
dimensions: {
status: dimension.string(),
},
measures: {
mrr: measure.sum('mrr'),
},
});With rowResolution:
- Every query on the dataset reads
FROM saas.subscriptions FINAL, including metric, derived and window queries. - Joins that target the dataset read it with
FINALtoo, so a versioned dimension table can't fan out or pick a stale row. checkRelationshipscounts keys on the resolved rows.- For
ReplacingMergeTree(ver, is_deleted), rows whose latest version is deleted disappear. A plainWHERE is_deleted = 0can't do this, because it keeps a deleted key's earlier versions. - It survives publishing to a deployment contract (MCP, or hypequery Cloud once it launches) unchanged: the published dataset reads exactly as it does locally.
Name the engine to also keep only current rows after FINAL: rows whose deletion column is 0, for a deletion column the engine doesn't declare, or whose sign column is 1 in a collapsing table:
export const Users = dataset('users', {
source: 'saas.users',
rowResolution: { kind: 'final', engine: 'replacing', deletedColumn: 'is_deleted' },
// For CollapsingMergeTree: { kind: 'final', engine: 'collapsing', signColumn: 'sign' }
dimensions: { id: dimension.string(), plan: dimension.string() },
measures: { users: measure.count('id') },
});FINAL makes queries do more work at read time. ClickHouse keeps it fast when the partition key separates keys and do_not_merge_across_partitions_select_final is enabled.
Queries that read FINAL send optimize_move_to_prewhere_if_final = 0, the server default, so no filter can run before FINAL and match an older version of a row. A readonly = 1 connection accepts it unless its profile turns that option on, in which case these queries are refused rather than answered from old rows.
A dataset with rowResolution needs the query-builder execution path. Semantic backends refuse it rather than serving unresolved rows.
final: true is the deprecated spelling of rowResolution: { kind: 'final' }. It still works, reads the same way and logs a one-time warning.
Module pattern
Define datasets at the module level and export them from dedicated files.
// datasets/orders.ts
export const Orders = dataset('orders', {
source: 'orders',
tenantKey: 'tenant_id',
timeKey: 'created_at',
dimensions: {
id: dimension.number(),
status: dimension.string(),
createdAt: dimension.timestamp({ column: 'created_at' }),
},
measures: {
revenue: measure.sum('amount'),
},
});Public shape
A dataset can include:
sourcefinaltenantKeytimeKeydimensionsmeasuresfiltersrelationshipslimits
See Dimensions, Measures, Metrics, and Relationships for the main building blocks.