@hypequery/datasets 0.13.2 → 0.13.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +46 -427
  2. package/package.json +19 -5
package/README.md CHANGED
@@ -1,477 +1,96 @@
1
1
  # @hypequery/datasets
2
2
 
3
- Type-safe semantic analytics definitions for Hypequery.
3
+ The code-first TypeScript semantic layer for ClickHouse.
4
4
 
5
- Use this package to define datasets, dimensions, measures, metrics, derived metrics, and runtime validation rules in TypeScript. `@hypequery/datasets` owns semantic meaning and planning; `@hypequery/clickhouse` owns query construction and execution; `@hypequery/serve` owns HTTP/runtime delivery.
5
+ `@hypequery/datasets` gives product teams one trusted definition for dimensions, measures, metrics, relationships, time grains, and multi-tenant isolation. Keep analytics meaning in the same repo as your application, then reuse it in backend jobs, HTTP APIs, React dashboards, and MCP tools for AI agents.
6
6
 
7
- `@hypequery/datasets` is the semantic layer. It is useful when you want analytics concepts in TypeScript rather than in YAML, raw SQL strings, or a separate BI server:
7
+ It builds on the `@hypequery/clickhouse` type-safe query builder, so semantic models and direct ClickHouse queries share generated schema types.
8
8
 
9
- - datasets map physical tables or views to typed business fields
10
- - dimensions and measures define the allowed query surface
11
- - metrics name reusable business calculations
12
- - derived metrics compose same-dataset metrics with symbolic formula helpers
13
- - runtime validation rejects invalid dimensions, filters, ordering, limits, tenant filters, and derived metric plans before SQL is executed
9
+ No YAML project. No separate semantic-layer server. Just typed, testable product analytics in code.
14
10
 
15
11
  ## Install
16
12
 
17
13
  ```bash
18
- npm install @hypequery/datasets
19
- # or
20
- pnpm add @hypequery/datasets
14
+ npm install @hypequery/datasets @hypequery/clickhouse
21
15
  ```
22
16
 
23
- For ClickHouse execution, also install the ClickHouse backend package:
24
-
25
- ```bash
26
- npm install @hypequery/clickhouse
27
- ```
28
-
29
- ## Quick Start
17
+ ## Define analytics once
30
18
 
31
19
  ```ts
32
- import {
33
- dataset,
34
- dimension,
35
- divide,
36
- eq,
37
- measure,
38
- nullIfZero,
39
- createDatasetClient,
40
- } from '@hypequery/datasets';
41
- import { createQueryBuilder } from '@hypequery/clickhouse';
20
+ import { dataset, dimension, measure } from '@hypequery/datasets';
42
21
 
43
- const Orders = dataset('orders', {
22
+ export const Orders = dataset('orders', {
44
23
  source: 'orders',
45
24
  tenantKey: 'tenant_id',
46
25
  timeKey: 'created_at',
47
26
  dimensions: {
48
- id: dimension.number(),
49
- tenantId: dimension.string({ column: 'tenant_id' }),
27
+ region: dimension.string(),
50
28
  status: dimension.string(),
51
- country: dimension.string(),
52
29
  createdAt: dimension.timestamp({ column: 'created_at' }),
53
30
  },
54
31
  measures: {
55
32
  revenue: measure.sum('amount'),
56
33
  orderCount: measure.count('id'),
57
- completedRevenue: measure.sum('amount', {
58
- filters: [eq('status', 'completed')],
59
- }),
60
- },
61
- filters: {
62
- status: {
63
- __type: 'filter_definition',
64
- field: 'status',
65
- operators: ['eq', 'neq', 'in', 'notIn'],
66
- },
67
- },
68
- });
69
-
70
- const revenue = Orders.metric('revenue', { measure: 'revenue' });
71
- const orderCount = Orders.metric('orderCount', { measure: 'orderCount' });
72
-
73
- const averageOrderValue = Orders.metric('averageOrderValue', {
74
- uses: { revenue, orderCount },
75
- formula: ({ revenue, orderCount }) =>
76
- divide(revenue, nullIfZero(orderCount)),
77
- });
78
-
79
- const db = createQueryBuilder({
80
- url: process.env.CLICKHOUSE_URL,
81
- username: process.env.CLICKHOUSE_USER,
82
- password: process.env.CLICKHOUSE_PASSWORD,
83
- database: process.env.CLICKHOUSE_DATABASE,
84
- });
85
-
86
- const analytics = createDatasetClient({ queryBuilder: db });
87
-
88
- const result = await analytics.execute(revenue, {
89
- dimensions: ['country'],
90
- filters: [eq('status', 'completed')],
91
- orderBy: [{ field: 'revenue', direction: 'desc' }],
92
- limit: 10,
93
- }, {
94
- runtime: {
95
- tenant: 'tenant_123',
96
- },
97
- });
98
-
99
- const datasetResult = await analytics.execute(Orders, {
100
- dimensions: ['country', 'status'],
101
- measures: ['revenue', 'orderCount'],
102
- filters: [eq('status', 'completed')],
103
- });
104
-
105
- const monthlySql = analytics.toSQL(revenue.by('month'), {
106
- dimensions: ['country'],
107
- }, {
108
- runtime: {
109
- tenant: 'tenant_123',
110
- },
111
- });
112
- ```
113
-
114
- ## Public API
115
-
116
- ### `dataset(name, config)`
117
-
118
- Creates a typed semantic model over a source table or view.
119
-
120
- ```ts
121
- const Orders = dataset('orders', {
122
- source: 'orders',
123
- tenantKey: 'tenant_id',
124
- timeKey: 'created_at',
125
- dimensions: {
126
- id: dimension.number(),
127
- createdAt: dimension.timestamp({ column: 'created_at' }),
128
- },
129
- measures: {
130
- revenue: measure.sum('amount'),
34
+ p95OrderValue: measure.percentile('amount', 0.95),
35
+ latestStatus: measure.argMax('status', 'createdAt'),
131
36
  },
132
37
  });
133
- ```
134
-
135
- `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.
136
-
137
- If `tenantKey` is set, queries against the dataset require runtime tenant context. This is fail-closed: Hypequery will reject metric and dataset queries that do not include a tenant runtime value or an explicitly enabled cross-tenant scope.
138
-
139
- ### Dimensions
140
-
141
- ```ts
142
- dimensions: {
143
- id: dimension.number(),
144
- status: dimension.string({ label: 'Status' }),
145
- isTrial: dimension.boolean({ column: 'is_trial' }),
146
- createdAt: dimension.timestamp({ column: 'created_at' }),
147
- }
148
- ```
149
-
150
- Dimension helpers are:
151
-
152
- - `dimension.string(opts?)`
153
- - `dimension.number(opts?)`
154
- - `dimension.boolean(opts?)`
155
- - `dimension.timestamp(opts?)`
156
-
157
- Use `opts.column` when the semantic field name differs from the physical column. `opts.sql` exists for SQL-backed dimensions, but schema compatibility can only inspect simple column references and reports a warning for complex SQL expressions.
158
-
159
- ### Measures
160
38
 
161
- ```ts
162
- measures: {
163
- revenue: measure.sum('amount'),
164
- orderCount: measure.count('id'),
165
- uniqueCustomers: measure.countDistinct('customerId'),
166
- averageAmount: measure.avg('amount'),
167
- minAmount: measure.min('amount'),
168
- maxAmount: measure.max('amount'),
169
- }
170
- ```
171
-
172
- Filtered measures use semantic filter helpers:
173
-
174
- ```ts
175
- import { eq, measure } from '@hypequery/datasets';
176
-
177
- const Orders = dataset('orders', {
178
- source: 'orders',
179
- dimensions: {
180
- status: dimension.string(),
181
- },
182
- measures: {
183
- completedRevenue: measure.sum('amount', {
184
- filters: [eq('status', 'completed')],
185
- }),
186
- },
187
- });
188
- ```
189
-
190
- ### Metrics
191
-
192
- Metrics are attached to a dataset and are defined from measures.
193
-
194
- ```ts
195
- const revenue = Orders.metric('revenue', {
39
+ export const revenue = Orders.metric('revenue', {
196
40
  measure: 'revenue',
197
41
  label: 'Revenue',
198
42
  });
199
43
  ```
200
44
 
201
- Derived metrics compose base metrics from the same dataset.
202
-
203
- ```ts
204
- const averageOrderValue = Orders.metric('averageOrderValue', {
205
- uses: { revenue, orderCount },
206
- formula: ({ revenue, orderCount }) =>
207
- divide(revenue, nullIfZero(orderCount)),
208
- });
209
- ```
210
-
211
- Cross-dataset derived metrics and derived-from-derived metrics are intentionally rejected in the current public surface.
45
+ The model is now the contract. Callers can only use the dimensions, measures, filters, and relationships you publish. A declared `tenantKey` requires trusted runtime scope and rejects unscoped execution.
212
46
 
213
- ### Metric Queries vs Dataset Queries
214
-
215
- Metrics and datasets support two related access patterns:
216
-
217
- - Metric queries execute one named metric at a time. They are best for reusable product metrics such as `revenue`, `averageOrderValue`, or `monthlyRevenue`, while still allowing valid dimensions, filters, order fields, limits, and time grains.
218
- - Dataset queries execute an ad-hoc selection of dimensions and measures from one dataset. They are best when callers need Cube-style flexibility within the same dataset, such as grouping by `country` and `status` while selecting both `revenue` and `orderCount`.
219
-
220
- ```ts
221
- await analytics.execute(revenue, {
222
- dimensions: ['country'],
223
- filters: [eq('status', 'completed')],
224
- });
225
-
226
- await analytics.execute(Orders, {
227
- dimensions: ['country', 'status'],
228
- measures: ['revenue', 'orderCount'],
229
- });
230
- ```
231
-
232
- Both paths use the same dataset definition and validation rules. Metric queries provide named, reusable business contracts; dataset queries provide same-dataset exploration.
233
-
234
- ### Time Grains
235
-
236
- Use `.by(grain)` on a metric when the dataset has a `timeKey`.
47
+ ## Query it anywhere
237
48
 
238
49
  ```ts
239
- const monthlyRevenue = revenue.by('month');
240
-
241
- await analytics.execute(monthlyRevenue, {
242
- dimensions: ['country'],
243
- }, {
244
- runtime: {
245
- tenant: 'tenant_123',
50
+ const result = await analytics.execute(
51
+ revenue,
52
+ {
53
+ dimensions: ['region'],
54
+ orderBy: [{ field: 'revenue', direction: 'desc' }],
55
+ limit: 10,
246
56
  },
247
- });
248
- ```
249
-
250
- Supported grains are `day`, `week`, `month`, `quarter`, and `year`.
251
-
252
- ### Runtime Tenancy
253
-
254
- Runtime tenancy uses the dataset `tenantKey` and a runtime tenant identity.
255
-
256
- ```ts
257
- const Orders = dataset('orders', {
258
- source: 'orders',
259
- tenantKey: 'tenant_id',
260
- dimensions: {
261
- tenantId: dimension.string({ column: 'tenant_id' }),
262
- country: dimension.string(),
57
+ {
58
+ runtime: { tenant: session.accountId },
263
59
  },
264
- measures: {
265
- revenue: measure.sum('amount'),
266
- },
267
- });
268
-
269
- const revenue = Orders.metric('revenue', { measure: 'revenue' });
270
-
271
- await analytics.execute(revenue, {}, {
272
- runtime: {
273
- tenant: 'tenant_123',
274
- },
275
- });
276
- ```
277
-
278
- Here `tenantKey` is the physical column and `runtime.tenant` is the trusted runtime value. Together they produce a tenant predicate equivalent to:
279
-
280
- ```sql
281
- WHERE tenant_id = 'tenant_123'
282
- ```
283
-
284
- If a dataset has `tenantKey`, runtime tenant context is required:
285
-
286
- ```ts
287
- await analytics.execute(revenue);
288
- // Error: Dataset "orders" requires runtime tenant scoping.
289
- ```
290
-
291
- You can also provide a trusted set of tenants for admin dashboards that are scoped to multiple accounts:
292
-
293
- ```ts
294
- await analytics.execute(revenue, {}, {
295
- runtime: {
296
- tenant: { in: ['tenant_123', 'tenant_456'] },
297
- },
298
- });
299
- ```
300
-
301
- This produces a tenant predicate equivalent to:
302
-
303
- ```sql
304
- WHERE tenant_id IN ('tenant_123', 'tenant_456')
305
- ```
306
-
307
- When runtime tenancy is active, explicit filters on the tenant field are rejected. This prevents duplicate or conflicting tenant predicates:
308
-
309
- ```ts
310
- await analytics.execute(revenue, {
311
- filters: [eq('tenantId', 'tenant_123')],
312
- }, {
313
- runtime: {
314
- tenant: 'tenant_123',
315
- },
316
- });
317
- // Error: Cannot filter on tenant field "tenantId" when runtime tenancy enforcement is active.
318
- ```
319
-
320
- The runtime integration should provide tenant identity from trusted server/session state. Do not accept tenant ids from end-user query input. In `@hypequery/serve`, regular hand-written queries can use Serve tenant auto-injection, but semantic dataset and metric endpoints pass tenant identity to `@hypequery/datasets`, and datasets injects the filter from `tenantKey`.
321
-
322
- For jobs, admin tools, or reporting surfaces that intentionally query across tenants, use `runtime.tenant: { scope: 'all' }`:
323
-
324
- ```ts
325
- await analytics.execute(revenue, {}, {
326
- runtime: {
327
- tenant: { scope: 'all' },
328
- },
329
- });
330
- ```
331
-
332
- `scope: 'all'` omits the tenant predicate for every `tenantKey` dataset touched by the semantic query. Use this only in trusted contexts like background jobs or admin dashboards, not in request-facing clients or MCP servers.
333
-
334
- ### Relationships
335
-
336
- Relationships can be stored as semantic metadata and exposed through introspection.
337
-
338
- ```ts
339
- import { belongsTo } from '@hypequery/datasets';
340
-
341
- const Orders = dataset('orders', {
342
- source: 'orders',
343
- dimensions: {
344
- customerId: dimension.string({ column: 'customer_id' }),
345
- },
346
- relationships: {
347
- customer: belongsTo(() => Customers, { from: 'customerId', to: 'id' }),
348
- },
349
- });
60
+ );
350
61
  ```
351
62
 
352
- For this release, relationship-aware query execution is not shipped. Query execution is same-dataset only. Relationship metadata is useful for documentation, agents, and schema compatibility checks.
353
-
354
- ## Execution
355
-
356
- Use `createDatasetClient` from `@hypequery/datasets` with a backend implementation to execute semantic targets.
357
-
358
- ```ts
359
- const validation = analytics.validate(revenue, {
360
- dimensions: ['country'],
361
- }, {
362
- runtime: {
363
- tenant: 'tenant_123',
364
- },
365
- });
366
-
367
- const sql = analytics.toSQL(revenue, {
368
- dimensions: ['country'],
369
- }, {
370
- runtime: {
371
- tenant: 'tenant_123',
372
- },
373
- });
374
-
375
- const result = await analytics.execute(revenue, {
376
- dimensions: ['country'],
377
- }, {
378
- runtime: {
379
- tenant: 'tenant_123',
380
- },
381
- });
382
-
383
- const datasetResult = await analytics.execute(Orders, {
384
- dimensions: ['country'],
385
- measures: ['revenue'],
386
- }, {
387
- runtime: {
388
- tenant: 'tenant_123',
389
- },
390
- });
391
- ```
392
-
393
- The semantic client validates dimensions, filters, order fields, limits, time grain requirements, tenant filtering, and derived metric plans before execution.
394
-
395
- ### ClickHouse
396
-
397
- For ClickHouse, pass a query builder from `@hypequery/clickhouse`. This is the recommended path: the dataset client reuses the same connection and builder you use for hand-written queries.
398
-
399
- ```ts
400
- import { createDatasetClient } from '@hypequery/datasets';
401
- import { createQueryBuilder } from '@hypequery/clickhouse';
402
-
403
- const db = createQueryBuilder({
404
- url: process.env.CLICKHOUSE_URL,
405
- username: process.env.CLICKHOUSE_USER,
406
- password: process.env.CLICKHOUSE_PASSWORD,
407
- database: process.env.CLICKHOUSE_DATABASE,
408
- });
409
-
410
- const analytics = createDatasetClient({ queryBuilder: db });
411
-
412
- await analytics.execute(revenue, { dimensions: ['country'] }, {
413
- runtime: {
414
- tenant: 'tenant_123',
415
- },
416
- });
417
- ```
418
-
419
- ### Advanced: SemanticBackend protocol
420
-
421
- `createDatasetClient` also accepts a `backend` implementing the database-agnostic `SemanticBackend` interface. For ClickHouse, `createBackend` from `@hypequery/clickhouse/datasets` builds one from connection config — use it when you want a standalone backend instance instead of sharing a query builder.
422
-
423
- ```ts
424
- import { createDatasetClient } from '@hypequery/datasets';
425
- import { createBackend } from '@hypequery/clickhouse/datasets';
426
-
427
- const analytics = createDatasetClient({
428
- backend: createBackend({
429
- url: process.env.CLICKHOUSE_URL,
430
- username: process.env.CLICKHOUSE_USER,
431
- password: process.env.CLICKHOUSE_PASSWORD,
432
- database: process.env.CLICKHOUSE_DATABASE,
433
- }),
434
- });
435
- ```
436
-
437
- The same `SemanticBackend` interface enables support for other databases. Future packages like `@hypequery/duckdb` or `@hypequery/postgres` would follow the same pattern.
438
-
439
- ## Integration Surfaces
440
-
441
- Dataset definitions can be reused in several places:
442
-
443
- - direct execution with `createDatasetClient(...)`
444
- - SQL inspection with `analytics.toSQL(...)`
445
- - runtime validation with `analytics.validate(...)`
446
- - HTTP metric and dataset endpoints through `@hypequery/serve`
447
- - agent-facing tools through `@hypequery/mcp`
448
-
449
- ## Serve Integration
450
-
451
- `@hypequery/serve` can expose metric and dataset endpoints from dataset definitions. Dataset endpoint planning uses `@hypequery/datasets/internal` as a package-integration boundary.
63
+ The same definition can become:
452
64
 
453
- Do not import `@hypequery/datasets/internal` in application code unless you are integrating Hypequery packages. It is intentionally not the public user-facing API.
65
+ - a named KPI or flexible dataset query;
66
+ - a validated `@hypequery/serve` endpoint;
67
+ - a typed `@hypequery/react` hook;
68
+ - an OpenAI, AI SDK, or MCP tool schema;
69
+ - a stable semantic contract for CI and deployment.
454
70
 
455
- Metric endpoints expose named metrics. Dataset endpoints expose same-dataset ad-hoc dimensions and measures. Both surfaces are generated from dataset contracts, so invalid fields are rejected before execution.
71
+ ## Analytics teams actually need
456
72
 
457
- ## Agent Integration
73
+ - sums, counts, distinct counts, averages, min, and max;
74
+ - percentiles, median, `argMax`, `argMin`, standard deviation, and variance;
75
+ - filtered measures and derived metric formulas;
76
+ - daily through yearly time grains;
77
+ - fail-closed multi-tenant analytics;
78
+ - one-hop typed `belongsTo` and `hasOne` dimensions;
79
+ - validated filtering, sorting, and pagination.
458
80
 
459
- `@hypequery/mcp` exposes dataset contracts, metric queries, and dataset queries over Model Context Protocol. This package does not run an MCP server directly; it provides the semantic definitions and execution client that the MCP package consumes.
81
+ See the [current capability matrix](https://hypequery.com/docs/capabilities) for exact syntax and the complete shipped surface.
460
82
 
461
- ## Current Scope And Limits
83
+ ## Why code-first
462
84
 
463
- The current semantic execution surface is intentionally scoped:
85
+ Metric changes travel through the workflow your team already trusts: TypeScript, code review, tests, CI, and version control. Backend, frontend, and agent consumers stop maintaining their own definition of “revenue.”
464
86
 
465
- - `dataset.query(...)` is not public API
466
- - Root exports for dataset endpoint execution helpers are not public API
467
- - Deep imports from package internals are not application API
468
- - Automatic relationship JOIN execution is not shipped
469
- - Cross-dataset derived metrics are rejected
470
- - Derived-from-derived metrics are rejected
471
- - Pre-aggregations or materialized rollups are not implemented
472
- - BI tool protocol compatibility is not implemented
87
+ ## Learn more
473
88
 
474
- Relationship metadata is available for documentation, agents, and compatibility checks, but query execution is same-dataset only.
89
+ - [Datasets overview](https://hypequery.com/docs/datasets/overview)
90
+ - [Measures and metrics](https://hypequery.com/docs/datasets/measures)
91
+ - [Multi-tenancy](https://hypequery.com/docs/datasets/multi-tenancy)
92
+ - [Relationships](https://hypequery.com/docs/datasets/relationships)
93
+ - [MCP tool generation](https://hypequery.com/docs/datasets/tool-generation)
475
94
 
476
95
  ## License
477
96
 
package/package.json CHANGED
@@ -1,7 +1,20 @@
1
1
  {
2
2
  "name": "@hypequery/datasets",
3
- "version": "0.13.2",
4
- "description": "Semantic layer for defining datasets, metrics, and semantic queries",
3
+ "version": "0.13.4",
4
+ "description": "Code-first TypeScript semantic layer for ClickHouse datasets, metrics, multi-tenancy, and AI agents",
5
+ "keywords": [
6
+ "semantic-layer",
7
+ "clickhouse",
8
+ "typescript",
9
+ "analytics",
10
+ "query-builder",
11
+ "orm",
12
+ "metrics",
13
+ "datasets",
14
+ "multi-tenant",
15
+ "mcp",
16
+ "ai-agents"
17
+ ],
5
18
  "main": "dist/index.js",
6
19
  "types": "dist/index.d.ts",
7
20
  "type": "module",
@@ -29,7 +42,7 @@
29
42
  "dist"
30
43
  ],
31
44
  "dependencies": {
32
- "@hypequery/protocol": "^0.10.0",
45
+ "@hypequery/protocol": "^0.11.0",
33
46
  "@noble/hashes": "^1.8.0",
34
47
  "zod": "^3.22.4"
35
48
  },
@@ -38,11 +51,12 @@
38
51
  "typescript": "^5.7.3",
39
52
  "@vitest/coverage-v8": "^3.2.6",
40
53
  "vitest": "^3.2.6",
41
- "@hypequery/protocol-conformance": "^0.9.1"
54
+ "@hypequery/protocol-conformance": "^0.10.0"
42
55
  },
43
56
  "repository": {
44
57
  "type": "git",
45
- "url": "https://github.com/hypequery/hypequery.git"
58
+ "url": "git+https://github.com/hypequery/hypequery.git",
59
+ "directory": "packages/datasets"
46
60
  },
47
61
  "homepage": "https://hypequery.com",
48
62
  "bugs": {