@hypequery/datasets 0.1.0 → 0.2.0

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 (83) hide show
  1. package/README.md +498 -0
  2. package/dist/api.type-test.d.ts +2 -0
  3. package/dist/api.type-test.d.ts.map +1 -0
  4. package/dist/api.type-test.js +103 -0
  5. package/dist/constants.d.ts +9 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/constants.js +11 -0
  8. package/dist/dataset-query.d.ts +16 -0
  9. package/dist/dataset-query.d.ts.map +1 -0
  10. package/dist/dataset-query.js +56 -0
  11. package/dist/dataset.d.ts +1 -1
  12. package/dist/dataset.d.ts.map +1 -1
  13. package/dist/dataset.js +22 -157
  14. package/dist/executor.d.ts +42 -14
  15. package/dist/executor.d.ts.map +1 -1
  16. package/dist/executor.js +188 -36
  17. package/dist/formulas.d.ts +1 -1
  18. package/dist/formulas.d.ts.map +1 -1
  19. package/dist/formulas.js +27 -12
  20. package/dist/in-memory-backend.d.ts +5 -0
  21. package/dist/in-memory-backend.d.ts.map +1 -0
  22. package/dist/in-memory-backend.js +221 -0
  23. package/dist/index.d.ts +6 -4
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +3 -4
  26. package/dist/internal.d.ts +23 -0
  27. package/dist/internal.d.ts.map +1 -0
  28. package/dist/internal.js +19 -0
  29. package/dist/measure.d.ts.map +1 -1
  30. package/dist/measure.js +1 -0
  31. package/dist/query-builder-protocol.d.ts +2 -2
  32. package/dist/query-builder-protocol.d.ts.map +1 -1
  33. package/dist/query-builder-protocol.js +1 -1
  34. package/dist/query-helpers.d.ts +12 -12
  35. package/dist/query-helpers.d.ts.map +1 -1
  36. package/dist/query-planner.d.ts +9 -7
  37. package/dist/query-planner.d.ts.map +1 -1
  38. package/dist/query-planner.js +26 -9
  39. package/dist/registry.d.ts +1 -1
  40. package/dist/registry.js +1 -1
  41. package/dist/relationships.d.ts +1 -1
  42. package/dist/relationships.js +1 -1
  43. package/dist/semantic-plan.d.ts +82 -0
  44. package/dist/semantic-plan.d.ts.map +1 -0
  45. package/dist/semantic-plan.js +1 -0
  46. package/dist/semantic-planner.d.ts +5 -0
  47. package/dist/semantic-planner.d.ts.map +1 -0
  48. package/dist/semantic-planner.js +155 -0
  49. package/dist/sql-utils.d.ts +1 -1
  50. package/dist/sql-utils.js +4 -4
  51. package/dist/types.d.ts +130 -52
  52. package/dist/types.d.ts.map +1 -1
  53. package/dist/utils/dataset-contract.d.ts +3 -0
  54. package/dist/utils/dataset-contract.d.ts.map +1 -0
  55. package/dist/utils/dataset-contract.js +30 -0
  56. package/dist/utils/dataset-metric-ref.d.ts +9 -0
  57. package/dist/utils/dataset-metric-ref.d.ts.map +1 -0
  58. package/dist/utils/dataset-metric-ref.js +39 -0
  59. package/dist/utils/dataset-normalization.d.ts +10 -0
  60. package/dist/utils/dataset-normalization.d.ts.map +1 -0
  61. package/dist/utils/dataset-normalization.js +35 -0
  62. package/dist/utils/dataset-query-validation.d.ts +4 -0
  63. package/dist/utils/dataset-query-validation.d.ts.map +1 -0
  64. package/dist/utils/dataset-query-validation.js +96 -0
  65. package/dist/utils/dataset-validation.d.ts +6 -0
  66. package/dist/utils/dataset-validation.d.ts.map +1 -0
  67. package/dist/utils/dataset-validation.js +42 -0
  68. package/dist/utils/derived-cte-validation.d.ts +3 -0
  69. package/dist/utils/derived-cte-validation.d.ts.map +1 -0
  70. package/dist/utils/derived-cte-validation.js +32 -0
  71. package/dist/utils/filtered-aggregation-sql.d.ts +5 -0
  72. package/dist/utils/filtered-aggregation-sql.d.ts.map +1 -0
  73. package/dist/utils/filtered-aggregation-sql.js +73 -0
  74. package/dist/utils/metric-handle.d.ts +11 -0
  75. package/dist/utils/metric-handle.d.ts.map +1 -0
  76. package/dist/utils/metric-handle.js +36 -0
  77. package/dist/utils/pagination.d.ts +17 -0
  78. package/dist/utils/pagination.d.ts.map +1 -0
  79. package/dist/utils/pagination.js +23 -0
  80. package/dist/utils/tenant-runtime.d.ts +14 -0
  81. package/dist/utils/tenant-runtime.d.ts.map +1 -0
  82. package/dist/utils/tenant-runtime.js +36 -0
  83. package/package.json +14 -2
package/README.md ADDED
@@ -0,0 +1,498 @@
1
+ # @hypequery/datasets
2
+
3
+ Type-safe semantic analytics definitions for Hypequery.
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.
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:
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
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install @hypequery/datasets
19
+ # or
20
+ pnpm add @hypequery/datasets
21
+ ```
22
+
23
+ For ClickHouse execution, also install the ClickHouse backend package:
24
+
25
+ ```bash
26
+ npm install @hypequery/clickhouse
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ ```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';
42
+
43
+ const Orders = dataset('orders', {
44
+ source: 'orders',
45
+ tenantKey: 'tenant_id',
46
+ timeKey: 'created_at',
47
+ dimensions: {
48
+ id: dimension.number(),
49
+ tenantId: dimension.string({ column: 'tenant_id' }),
50
+ status: dimension.string(),
51
+ country: dimension.string(),
52
+ createdAt: dimension.timestamp({ column: 'created_at' }),
53
+ },
54
+ measures: {
55
+ revenue: measure.sum('amount'),
56
+ 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'),
131
+ },
132
+ });
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
+
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', {
196
+ measure: 'revenue',
197
+ label: 'Revenue',
198
+ });
199
+ ```
200
+
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.
212
+
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`.
237
+
238
+ ```ts
239
+ const monthlyRevenue = revenue.by('month');
240
+
241
+ await analytics.execute(monthlyRevenue, {
242
+ dimensions: ['country'],
243
+ }, {
244
+ runtime: {
245
+ tenant: 'tenant_123',
246
+ },
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(),
263
+ },
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
+ });
350
+ ```
351
+
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
+ - schema compatibility checks through `@hypequery/schema`
449
+
450
+ ## Serve Integration
451
+
452
+ `@hypequery/serve` can expose metric and dataset endpoints from dataset definitions. Dataset endpoint planning uses `@hypequery/datasets/internal` as a package-integration boundary.
453
+
454
+ Do not import `@hypequery/datasets/internal` in application code unless you are integrating Hypequery packages. It is intentionally not the public user-facing API.
455
+
456
+ 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.
457
+
458
+ ## Agent Integration
459
+
460
+ `@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.
461
+
462
+ ## Schema Compatibility
463
+
464
+ Use `@hypequery/schema` to check whether physical schema changes break dataset definitions.
465
+
466
+ ```ts
467
+ import { checkDatasetsAgainstSchema } from '@hypequery/schema';
468
+
469
+ const report = checkDatasetsAgainstSchema({
470
+ snapshot,
471
+ datasets: [Orders],
472
+ });
473
+
474
+ if (!report.valid) {
475
+ console.error(report.diagnostics);
476
+ }
477
+ ```
478
+
479
+ The checker validates source tables/views, dimension columns, measure fields, tenant/time keys, filtered measure fields, numeric measure types, and relationship join columns. Complex SQL expressions are reported with explicit limitation warnings.
480
+
481
+ ## Current Scope And Limits
482
+
483
+ The current semantic execution surface is intentionally scoped:
484
+
485
+ - `dataset.query(...)` is not public API
486
+ - Root exports for dataset endpoint execution helpers are not public API
487
+ - Deep imports from package internals are not application API
488
+ - Automatic relationship JOIN execution is not shipped
489
+ - Cross-dataset derived metrics are rejected
490
+ - Derived-from-derived metrics are rejected
491
+ - Pre-aggregations or materialized rollups are not implemented
492
+ - BI tool protocol compatibility is not implemented
493
+
494
+ Relationship metadata is available for documentation, agents, and compatibility checks, but query execution is same-dataset only.
495
+
496
+ ## License
497
+
498
+ Apache-2.0.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=api.type-test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.type-test.d.ts","sourceRoot":"","sources":["../src/api.type-test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,103 @@
1
+ import { add, dataset, dimension, measure, eq, between, desc, createDatasetClient } from './index.js';
2
+ const Orders = dataset('orders', {
3
+ source: 'orders',
4
+ tenantKey: 'tenant_id',
5
+ timeKey: 'created_at',
6
+ dimensions: {
7
+ id: dimension.string(),
8
+ tenantId: dimension.string({ column: 'tenant_id' }),
9
+ status: dimension.string(),
10
+ amount: dimension.number(),
11
+ createdAt: dimension.timestamp({ column: 'created_at' }),
12
+ },
13
+ measures: {
14
+ revenue: measure.sum('amount'),
15
+ completedRevenue: measure.sum('amount', {
16
+ filters: [eq('status', 'completed')],
17
+ }),
18
+ },
19
+ });
20
+ const Customers = dataset('customers', {
21
+ source: 'customers',
22
+ dimensions: {
23
+ id: dimension.string(),
24
+ status: dimension.string(),
25
+ },
26
+ measures: {
27
+ customerCount: measure.count('id'),
28
+ },
29
+ });
30
+ const revenueMetric = Orders.metric('revenueMetric', { measure: 'revenue' });
31
+ const completedRevenueMetric = Orders.metric('completedRevenueMetric', { measure: 'completedRevenue' });
32
+ const averageRevenueMetric = Orders.metric('averageRevenueMetric', {
33
+ uses: { revenue: revenueMetric, completedRevenue: completedRevenueMetric },
34
+ formula: ({ revenue, completedRevenue }) => add(revenue, completedRevenue),
35
+ });
36
+ const customerCountMetric = Customers.metric('customerCountMetric', { measure: 'customerCount' });
37
+ const statusFilter = eq('status', 'completed');
38
+ const createdAtRange = between('createdAt', '2025-01-01', '2025-01-31');
39
+ const revenueSort = desc('revenueMetric');
40
+ Orders.metric('validDerivedMetric', {
41
+ uses: { revenue: revenueMetric },
42
+ formula: ({ revenue }) => add(revenue, revenue),
43
+ });
44
+ // @ts-expect-error derived metrics can only use base metrics from the same dataset.
45
+ Orders.metric('invalidCrossDatasetDerivedMetric', {
46
+ uses: { customerCount: customerCountMetric },
47
+ formula: () => add('customerCount', 'customerCount'),
48
+ });
49
+ // @ts-expect-error derived metrics can only use base metrics, not derived metric refs.
50
+ Orders.metric('invalidDerivedFromDerivedMetric', {
51
+ uses: { averageRevenue: averageRevenueMetric },
52
+ formula: () => add('averageRevenue', 'averageRevenue'),
53
+ });
54
+ const runtimeContext = {
55
+ runtime: {
56
+ tenant: 'tenant-1',
57
+ },
58
+ };
59
+ const legacyTenantRuntimeContext = {
60
+ runtime: {
61
+ tenant: { id: 'tenant-1' },
62
+ },
63
+ };
64
+ const tenantSetRuntimeContext = {
65
+ runtime: {
66
+ tenant: { in: ['tenant-1', 'tenant-2'] },
67
+ },
68
+ };
69
+ const crossTenantRuntimeContext = {
70
+ runtime: {
71
+ tenant: { scope: 'all' },
72
+ },
73
+ };
74
+ const builderFactory = {
75
+ table: () => ({
76
+ select: () => builderFactory.table('orders'),
77
+ sum: () => builderFactory.table('orders'),
78
+ count: () => builderFactory.table('orders'),
79
+ countDistinct: () => builderFactory.table('orders'),
80
+ avg: () => builderFactory.table('orders'),
81
+ min: () => builderFactory.table('orders'),
82
+ max: () => builderFactory.table('orders'),
83
+ where: () => builderFactory.table('orders'),
84
+ groupBy: () => builderFactory.table('orders'),
85
+ orderBy: () => builderFactory.table('orders'),
86
+ limit: () => builderFactory.table('orders'),
87
+ offset: () => builderFactory.table('orders'),
88
+ toSQLWithParams: () => ({ sql: 'SELECT 1', parameters: [] }),
89
+ execute: async () => [],
90
+ }),
91
+ rawQuery: async () => [],
92
+ };
93
+ const analytics = createDatasetClient({ queryBuilder: builderFactory });
94
+ const explicitAnalytics = analytics;
95
+ const datasetQuery = { dimensions: ['status'], measures: ['revenue'] };
96
+ analytics.validate(revenueMetric, { dimensions: ['status'] }, runtimeContext);
97
+ analytics.toSQL(completedRevenueMetric, { dimensions: ['status'] }, runtimeContext);
98
+ analytics.toSQL(revenueMetric, { orderBy: [desc('revenueMetric')] }, runtimeContext);
99
+ analytics.validate(Orders, datasetQuery, runtimeContext);
100
+ analytics.toSQL(Orders, datasetQuery, runtimeContext);
101
+ void analytics.execute(Orders, datasetQuery, runtimeContext);
102
+ void runtimeContext;
103
+ void explicitAnalytics;
@@ -6,4 +6,13 @@ import type { TimeGrain } from './types.js';
6
6
  * Maps time grain to ClickHouse date truncation functions.
7
7
  */
8
8
  export declare const GRAIN_FUNCTIONS: Record<TimeGrain, string>;
9
+ /**
10
+ * The set of time grains supported by the planner. Derived from
11
+ * {@link GRAIN_FUNCTIONS} so the two never drift apart.
12
+ */
13
+ export declare const SUPPORTED_TIME_GRAINS: TimeGrain[];
14
+ /**
15
+ * Narrowing guard for a runtime-provided grain value.
16
+ */
17
+ export declare function isSupportedTimeGrain(grain: unknown): grain is TimeGrain;
9
18
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAMrD,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAMrD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAmC,SAAS,EAAE,CAAC;AAEjF;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS,CAEvE"}
package/dist/constants.js CHANGED
@@ -11,3 +11,14 @@ export const GRAIN_FUNCTIONS = {
11
11
  quarter: 'toStartOfQuarter',
12
12
  year: 'toStartOfYear',
13
13
  };
14
+ /**
15
+ * The set of time grains supported by the planner. Derived from
16
+ * {@link GRAIN_FUNCTIONS} so the two never drift apart.
17
+ */
18
+ export const SUPPORTED_TIME_GRAINS = Object.keys(GRAIN_FUNCTIONS);
19
+ /**
20
+ * Narrowing guard for a runtime-provided grain value.
21
+ */
22
+ export function isSupportedTimeGrain(grain) {
23
+ return typeof grain === 'string' && Object.hasOwn(GRAIN_FUNCTIONS, grain);
24
+ }
@@ -0,0 +1,16 @@
1
+ import type { AnyDatasetInstance, DatasetQuery, DatasetQueryResult, ExecutionContext } from './types.js';
2
+ import type { QueryBuilderFactoryLike, QueryBuilderLike } from './query-builder-protocol.js';
3
+ import { type ValidationResult } from './validation.js';
4
+ export interface DatasetQueryExecutionOptions {
5
+ builderFactory: QueryBuilderFactoryLike;
6
+ context?: ExecutionContext;
7
+ /**
8
+ * Overrides the SQL `LIMIT` without affecting validation (which still uses
9
+ * `query.limit`). Used to over-fetch one row for pagination's `hasMore`.
10
+ */
11
+ executionLimit?: number;
12
+ }
13
+ export declare function validateDatasetQuery(ds: AnyDatasetInstance, query: DatasetQuery, context?: ExecutionContext): ValidationResult;
14
+ export declare function buildDatasetQueryBuilder(ds: AnyDatasetInstance, query: DatasetQuery, options: DatasetQueryExecutionOptions): QueryBuilderLike;
15
+ export declare function runDatasetQuery(ds: AnyDatasetInstance, query: DatasetQuery, options: DatasetQueryExecutionOptions): Promise<DatasetQueryResult>;
16
+ //# sourceMappingURL=dataset-query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataset-query.d.ts","sourceRoot":"","sources":["../src/dataset-query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EACjB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAQ7F,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAoBxD,MAAM,WAAW,4BAA4B;IAC3C,cAAc,EAAE,uBAAuB,CAAC;IACxC,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CAElB;AAED,wBAAgB,wBAAwB,CACtC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,4BAA4B,GACpC,gBAAgB,CAwClB;AAED,wBAAsB,eAAe,CACnC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,kBAAkB,CAAC,CAa7B"}