@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/dist/executor.js CHANGED
@@ -1,34 +1,37 @@
1
1
  /**
2
- * MetricExecutor resolves metrics to SQL and executes them.
2
+ * Semantic dataset client internals.
3
3
  *
4
- * Supports two modes:
5
- * 1. **Query builder** (recommended) pass a `QueryBuilderFactoryLike` and the
6
- * executor builds queries via the builder's fluent API, then calls `.execute()`.
7
- * 2. **Raw adapter** (deprecated) — pass a `MetricAdapter` with a `rawQuery` function
8
- * and the executor generates SQL strings manually.
9
- *
10
- * The executor stays DB-agnostic via duck-typed protocol interfaces.
4
+ * Public callers use `createDatasetClient(...).execute(...)`. The implementation
5
+ * stays database-agnostic via query-builder and backend protocol interfaces.
11
6
  */
12
7
  import { validateSQLIdentifier } from './sql-utils.js';
8
+ import { SUPPORTED_TIME_GRAINS, isSupportedTimeGrain } from './constants.js';
13
9
  import { validateFilterValue } from './validation.js';
14
10
  import { applyAggregationSpec, appendOrderLimitOffset, buildDimensionSelectionPlan, resolveFilterField, resolveTenantFilterColumn, } from './query-planner.js';
15
- // =============================================================================
16
- // VALIDATION
17
- // =============================================================================
18
- function validateQuery(metric, query) {
11
+ import { assertMetricHandle, getMetricGrain, getMetricRef, isTenantScopedFilter, } from './utils/metric-handle.js';
12
+ import { validateDerivedCteGrouping } from './utils/derived-cte-validation.js';
13
+ import { buildDatasetQueryBuilder, runDatasetQuery, validateDatasetQuery, } from './dataset-query.js';
14
+ import { buildDatasetPlan, buildMetricPlan, } from './semantic-planner.js';
15
+ import { getRuntimeTenantId, getRuntimeTenantPredicate, validateTenantRuntime, } from './utils/tenant-runtime.js';
16
+ import { applyPagination, overfetchLimit } from './utils/pagination.js';
17
+ function validateQuery(metric, query, context) {
19
18
  const errors = [];
20
- const ref = metric.__type === 'grained_metric_ref' ? metric.metric : metric;
19
+ const ref = getMetricRef(metric);
21
20
  const ds = ref.dataset;
22
21
  const dimensionNames = Object.keys(ds.dimensions);
23
22
  const filterNames = Object.keys(ds.filters).length > 0
24
23
  ? Object.keys(ds.filters)
25
24
  : dimensionNames;
26
- const grain = metric.__type === 'grained_metric_ref' ? metric.grain : query.by;
25
+ const grain = getMetricGrain(metric, query);
27
26
  const orderableFields = new Set([
28
27
  ...(query.dimensions ?? []),
29
28
  ref.name,
30
29
  ...(grain ? ['period'] : []),
31
30
  ]);
31
+ const tenantRuntimeError = validateTenantRuntime(ds, context);
32
+ if (tenantRuntimeError) {
33
+ errors.push(tenantRuntimeError);
34
+ }
32
35
  if (metric.__type === 'grained_metric_ref' && query.by && query.by !== metric.grain) {
33
36
  errors.push(`Metric "${ref.name}" is already grained by "${metric.grain}" and cannot be queried with by="${query.by}".`);
34
37
  }
@@ -50,6 +53,10 @@ function validateQuery(metric, query) {
50
53
  continue;
51
54
  }
52
55
  const resolvedField = ds.filters[filter.field]?.field ?? filter.field;
56
+ if (isTenantScopedFilter(ds, filter, context)) {
57
+ errors.push(`Cannot filter on tenant field "${filter.field}" when runtime tenancy enforcement is active.`);
58
+ continue;
59
+ }
53
60
  const fieldType = ds.dimensions[resolvedField]?.fieldType;
54
61
  if (fieldType) {
55
62
  const filterError = validateFilterValue(filter, fieldType);
@@ -68,6 +75,16 @@ function validateQuery(metric, query) {
68
75
  if (query.by && !ds.timeKey) {
69
76
  errors.push(`Cannot use "by" grain — dataset "${ds.name}" has no timeKey.`);
70
77
  }
78
+ // Validate grain is one the planner can bucket on
79
+ if (query.by && !isSupportedTimeGrain(query.by)) {
80
+ errors.push(`Unsupported time grain "${query.by}". Supported: ${SUPPORTED_TIME_GRAINS.join(', ')}`);
81
+ }
82
+ if (query.limit != null && (!Number.isInteger(query.limit) || query.limit < 0)) {
83
+ errors.push(`Invalid limit: expected a non-negative integer.`);
84
+ }
85
+ if (query.offset != null && (!Number.isInteger(query.offset) || query.offset < 0)) {
86
+ errors.push(`Invalid offset: expected a non-negative integer.`);
87
+ }
71
88
  // Validate limits
72
89
  if (ds.limits) {
73
90
  if (ds.limits.maxDimensions && (query.dimensions?.length ?? 0) > ds.limits.maxDimensions) {
@@ -79,10 +96,16 @@ function validateQuery(metric, query) {
79
96
  if (ds.limits.maxFilters && (query.filters?.length ?? 0) > ds.limits.maxFilters) {
80
97
  errors.push(`Too many filters (${query.filters?.length}). Max: ${ds.limits.maxFilters}`);
81
98
  }
99
+ if (ds.limits.maxResultSize && query.limit != null && query.limit > ds.limits.maxResultSize) {
100
+ errors.push(`Too many results requested (${query.limit}). Max: ${ds.limits.maxResultSize}`);
101
+ }
82
102
  }
83
103
  return { valid: errors.length === 0, errors };
84
104
  }
85
- export class MetricExecutor {
105
+ function isDatasetInstance(target) {
106
+ return !!target && typeof target === 'object' && '__type' in target && target.__type === 'dataset';
107
+ }
108
+ export class MetricQueryEngine {
86
109
  builderFactory;
87
110
  constructor(options) {
88
111
  this.builderFactory = options.builderFactory;
@@ -94,7 +117,8 @@ export class MetricExecutor {
94
117
  * Execute a metric query. Generates SQL, applies tenant/filter context, executes.
95
118
  */
96
119
  async run(metric, query = {}, context) {
97
- const validation = this.validate(metric, query);
120
+ assertMetricHandle(metric);
121
+ const validation = this.validate(metric, query, context);
98
122
  if (!validation.valid) {
99
123
  throw new Error(`Invalid metric query: ${validation.errors.join('; ')}`);
100
124
  }
@@ -105,8 +129,13 @@ export class MetricExecutor {
105
129
  * Generate SQL without executing.
106
130
  */
107
131
  toSQL(metric, query = {}, context) {
108
- const ref = metric.__type === 'grained_metric_ref' ? metric.metric : metric;
109
- const grain = metric.__type === 'grained_metric_ref' ? metric.grain : query.by ?? undefined;
132
+ assertMetricHandle(metric);
133
+ const validation = this.validate(metric, query, context);
134
+ if (!validation.valid) {
135
+ throw new Error(`Invalid metric query: ${validation.errors.join('; ')}`);
136
+ }
137
+ const ref = getMetricRef(metric);
138
+ const grain = getMetricGrain(metric, query);
110
139
  const spec = ref.spec;
111
140
  if (spec.__type === 'derived_metric_spec') {
112
141
  return this.buildDerivedSQLViaBuilder(ref, spec, query, grain, context).sql;
@@ -117,30 +146,55 @@ export class MetricExecutor {
117
146
  /**
118
147
  * Validate a metric query against the metric's contract.
119
148
  */
120
- validate(metric, query) {
121
- return validateQuery(metric, query);
149
+ validate(metric, query, context) {
150
+ assertMetricHandle(metric);
151
+ const queryValidation = validateQuery(metric, query, context);
152
+ if (!queryValidation.valid) {
153
+ return queryValidation;
154
+ }
155
+ const ref = getMetricRef(metric);
156
+ const grain = getMetricGrain(metric, query);
157
+ try {
158
+ if (ref.spec.__type === 'derived_metric_spec') {
159
+ this.buildDerivedSQLViaBuilder(ref, ref.spec, query, grain, context);
160
+ }
161
+ else {
162
+ this.buildBaseQuery(ref, ref.spec, ref.dataset, query, grain, context).toSQLWithParams();
163
+ }
164
+ }
165
+ catch (error) {
166
+ return {
167
+ valid: false,
168
+ errors: [error instanceof Error ? error.message : String(error)],
169
+ };
170
+ }
171
+ return queryValidation;
122
172
  }
123
173
  // ---------------------------------------------------------------------------
124
174
  // Query builder path
125
175
  // ---------------------------------------------------------------------------
126
176
  async runViaBuilder(metric, query, context, start) {
127
- const ref = metric.__type === 'grained_metric_ref' ? metric.metric : metric;
128
- const grain = metric.__type === 'grained_metric_ref' ? metric.grain : query.by ?? undefined;
177
+ const ref = getMetricRef(metric);
178
+ const grain = getMetricGrain(metric, query);
129
179
  const spec = ref.spec;
130
180
  const activeBuilderFactory = context?.runtime?.builderFactory ?? this.builderFactory;
181
+ // Over-fetch one row so we can report `hasMore` without a count query.
182
+ const buildQuery = { ...query, limit: overfetchLimit(query.limit) };
131
183
  if (spec.__type === 'derived_metric_spec') {
132
184
  // Derived metrics: build CTE via builder, outer query via string, execute via rawQuery
133
- const { sql, params } = this.buildDerivedSQLViaBuilder(ref, spec, query, grain, context);
134
- const data = await activeBuilderFactory.rawQuery(sql, params);
185
+ const { sql, params } = this.buildDerivedSQLViaBuilder(ref, spec, buildQuery, grain, context);
186
+ const rows = await activeBuilderFactory.rawQuery(sql, params);
135
187
  const timingMs = Date.now() - start;
136
- return { data, meta: { sql, timingMs, tenant: context?.runtime?.tenant?.id } };
188
+ const { data, pagination } = applyPagination(rows, query.limit, query.offset);
189
+ return { data, meta: { sql, timingMs, tenant: getRuntimeTenantId(context), rowCount: data.length, pagination } };
137
190
  }
138
191
  // Base metrics: fully use the builder's execute()
139
- const builder = this.buildBaseQuery(ref, spec, ref.dataset, query, grain, context);
192
+ const builder = this.buildBaseQuery(ref, spec, ref.dataset, buildQuery, grain, context);
140
193
  const { sql } = builder.toSQLWithParams();
141
- const data = await builder.execute();
194
+ const rows = await builder.execute();
142
195
  const timingMs = Date.now() - start;
143
- return { data, meta: { sql, timingMs, tenant: context?.runtime?.tenant?.id } };
196
+ const { data, pagination } = applyPagination(rows, query.limit, query.offset);
197
+ return { data, meta: { sql, timingMs, tenant: getRuntimeTenantId(context), rowCount: data.length, pagination } };
144
198
  }
145
199
  buildBaseQuery(ref, spec, ds, query, grain, context) {
146
200
  const activeBuilderFactory = context?.runtime?.builderFactory ?? this.builderFactory;
@@ -157,13 +211,15 @@ export class MetricExecutor {
157
211
  }
158
212
  // Tenant auto-injection
159
213
  const tenantColumn = resolveTenantFilterColumn(ds, context);
160
- const tenantId = context?.runtime?.tenant?.id;
161
- const tenantHandledByBuilder = context?.runtime?.tenant?.handledByBuilder === true;
162
- if (tenantId && tenantColumn && !tenantHandledByBuilder) {
163
- qb = qb.where(tenantColumn, 'eq', tenantId);
214
+ const tenantPredicate = getRuntimeTenantPredicate(context);
215
+ if (tenantPredicate && tenantColumn) {
216
+ qb = qb.where(tenantColumn, tenantPredicate.operator, tenantPredicate.value);
164
217
  }
165
218
  // User filters
166
219
  for (const filter of query.filters ?? []) {
220
+ if (isTenantScopedFilter(ds, filter, context)) {
221
+ throw new Error(`Cannot filter on tenant field "${filter.field}" when runtime tenancy enforcement is active.`);
222
+ }
167
223
  const resolvedField = resolveFilterField(ds, filter.field);
168
224
  qb = qb.where(resolvedField, filter.operator, filter.value);
169
225
  }
@@ -182,6 +238,7 @@ export class MetricExecutor {
182
238
  }
183
239
  // Base aggregations
184
240
  const refAliases = {};
241
+ const aggregateAliases = [];
185
242
  for (const [alias, baseMetric] of Object.entries(spec.uses)) {
186
243
  const baseSpec = baseMetric.spec;
187
244
  if (baseSpec.__type !== 'aggregation_spec') {
@@ -189,22 +246,29 @@ export class MetricExecutor {
189
246
  }
190
247
  cteBuilder = applyAggregationSpec(cteBuilder, ds, baseSpec, alias);
191
248
  refAliases[alias] = alias;
249
+ aggregateAliases.push(alias);
192
250
  }
193
251
  if (groupByParts.length > 0) {
194
252
  cteBuilder = cteBuilder.groupBy(groupByParts);
195
253
  }
196
254
  // Filters on CTE
197
255
  const tenantColumn = resolveTenantFilterColumn(ds, context);
198
- const tenantId = context?.runtime?.tenant?.id;
199
- const tenantHandledByBuilder = context?.runtime?.tenant?.handledByBuilder === true;
200
- if (tenantId && tenantColumn && !tenantHandledByBuilder) {
201
- cteBuilder = cteBuilder.where(tenantColumn, 'eq', tenantId);
256
+ const tenantPredicate = getRuntimeTenantPredicate(context);
257
+ if (tenantPredicate && tenantColumn) {
258
+ cteBuilder = cteBuilder.where(tenantColumn, tenantPredicate.operator, tenantPredicate.value);
202
259
  }
203
260
  for (const filter of query.filters ?? []) {
261
+ if (isTenantScopedFilter(ds, filter, context)) {
262
+ throw new Error(`Cannot filter on tenant field "${filter.field}" when runtime tenancy enforcement is active.`);
263
+ }
204
264
  const resolvedField = resolveFilterField(ds, filter.field);
205
265
  cteBuilder = cteBuilder.where(resolvedField, filter.operator, filter.value);
206
266
  }
207
267
  const { sql: cteSql, parameters: cteParams } = cteBuilder.toSQLWithParams();
268
+ const groupingErrors = validateDerivedCteGrouping(cteSql, aggregateAliases, groupByParts);
269
+ if (groupingErrors.length > 0) {
270
+ throw new Error(groupingErrors.join('; '));
271
+ }
208
272
  // Outer query: trivial SELECT from the CTE — stays as string concat
209
273
  // because table('base') would fail schema typing
210
274
  const outerSelectParts = [];
@@ -242,3 +306,91 @@ export class MetricExecutor {
242
306
  return { sql, params: cteParams };
243
307
  }
244
308
  }
309
+ export class DatasetClientImpl extends MetricQueryEngine {
310
+ backend;
311
+ constructor(options) {
312
+ if (!options.queryBuilder && !options.backend) {
313
+ throw new Error('createDatasetClient requires either queryBuilder or backend.');
314
+ }
315
+ super({
316
+ builderFactory: options.queryBuilder ?? {
317
+ table() {
318
+ throw new Error('This dataset client was created with a semantic backend, not a query builder.');
319
+ },
320
+ async rawQuery() {
321
+ throw new Error('This dataset client was created with a semantic backend, not a query builder.');
322
+ },
323
+ },
324
+ });
325
+ this.backend = options.backend;
326
+ }
327
+ planMetric(metric, query = {}, context) {
328
+ assertMetricHandle(metric);
329
+ const validation = validateQuery(metric, query, context);
330
+ if (!validation.valid) {
331
+ throw new Error(`Invalid metric query: ${validation.errors.join('; ')}`);
332
+ }
333
+ return buildMetricPlan(metric, query, context);
334
+ }
335
+ planDataset(ds, query = {}, context) {
336
+ return buildDatasetPlan(ds, query, context);
337
+ }
338
+ /**
339
+ * Execute a semantic target.
340
+ */
341
+ execute(target, query = {}, context) {
342
+ if (isDatasetInstance(target)) {
343
+ return this.executeDataset(target, query, context);
344
+ }
345
+ return this.executeMetric(target, query, context);
346
+ }
347
+ toSQL(target, query = {}, context) {
348
+ if (isDatasetInstance(target)) {
349
+ return this.toDatasetSQL(target, query, context);
350
+ }
351
+ return super.toSQL(target, query, context);
352
+ }
353
+ validate(target, query = {}, context) {
354
+ if (isDatasetInstance(target)) {
355
+ return validateDatasetQuery(target, query, context);
356
+ }
357
+ return super.validate(target, query, context);
358
+ }
359
+ executeMetric(metric, query, context) {
360
+ if (this.backend) {
361
+ const validation = validateQuery(metric, query, context);
362
+ if (!validation.valid) {
363
+ throw new Error(`Invalid metric query: ${validation.errors.join('; ')}`);
364
+ }
365
+ return this.backend.execute(this.planMetric(metric, query, context));
366
+ }
367
+ return this.run(metric, query, context);
368
+ }
369
+ executeDataset(ds, query, context) {
370
+ if (this.backend) {
371
+ const validation = this.validate(ds, query, context);
372
+ if (!validation.valid) {
373
+ throw new Error(`Invalid dataset query: ${validation.errors.join('; ')}`);
374
+ }
375
+ return this.backend.execute(this.planDataset(ds, query, context));
376
+ }
377
+ const validation = this.validate(ds, query, context);
378
+ if (!validation.valid) {
379
+ throw new Error(`Invalid dataset query: ${validation.errors.join('; ')}`);
380
+ }
381
+ return runDatasetQuery(ds, query, {
382
+ builderFactory: context?.runtime?.builderFactory ?? this.getBuilderFactory(),
383
+ context,
384
+ });
385
+ }
386
+ toDatasetSQL(ds, query, context) {
387
+ const builder = buildDatasetQueryBuilder(ds, query, {
388
+ builderFactory: context?.runtime?.builderFactory ?? this.getBuilderFactory(),
389
+ context,
390
+ });
391
+ return builder.toSQLWithParams().sql;
392
+ }
393
+ }
394
+ export function createDatasetClient(options) {
395
+ return new DatasetClientImpl(options);
396
+ }
@@ -2,7 +2,7 @@
2
2
  * Formula helpers for derived metrics.
3
3
  *
4
4
  * These are symbolic — they build FormulaExpr objects that get compiled to SQL
5
- * by the MetricExecutor. They do not produce raw SQL strings directly.
5
+ * by the semantic query engine. They do not produce raw SQL strings directly.
6
6
  *
7
7
  * @example
8
8
  * ```ts
@@ -1 +1 @@
1
- {"version":3,"file":"formulas.d.ts","sourceRoot":"","sources":["../src/formulas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAgB9C,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAEpF;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAEtF;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAEtF;AAED,wBAAgB,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAEjF;AAMD,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAE/D;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,WAAW,CAGtG;AAMD,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE,QAAQ,GAAE,MAAU,GAAG,WAAW,CAEhF;AAED,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAE1D;AAED,wBAAgB,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAEzD"}
1
+ {"version":3,"file":"formulas.d.ts","sourceRoot":"","sources":["../src/formulas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAkC9C,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAKpF;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAKtF;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAKtF;AAED,wBAAgB,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAKjF;AAMD,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAK/D;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,WAAW,CAMtG;AAMD,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE,QAAQ,GAAE,MAAU,GAAG,WAAW,CAKhF;AAED,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAK1D;AAED,wBAAgB,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAKzD"}
package/dist/formulas.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Formula helpers for derived metrics.
3
3
  *
4
4
  * These are symbolic — they build FormulaExpr objects that get compiled to SQL
5
- * by the MetricExecutor. They do not produce raw SQL strings directly.
5
+ * by the semantic query engine. They do not produce raw SQL strings directly.
6
6
  *
7
7
  * @example
8
8
  * ```ts
@@ -12,8 +12,8 @@
12
12
  * });
13
13
  * ```
14
14
  */
15
- function expr(sqlFn) {
16
- return { __type: 'formula_expr', toSQL: sqlFn };
15
+ function expr(expression, sqlFn) {
16
+ return { __type: 'formula_expr', expression, toSQL: sqlFn };
17
17
  }
18
18
  /** Check if a value is a FormulaExpr. */
19
19
  function resolveArg(a) {
@@ -21,40 +21,55 @@ function resolveArg(a) {
21
21
  return a;
22
22
  return a.toSQL();
23
23
  }
24
+ function isFormulaExpr(value) {
25
+ return typeof value === 'object'
26
+ && value !== null
27
+ && '__type' in value
28
+ && value.__type === 'formula_expr';
29
+ }
30
+ function resolveExpression(a) {
31
+ if (isFormulaExpr(a)) {
32
+ return a.expression;
33
+ }
34
+ if (typeof a === 'string') {
35
+ return { kind: 'ref', name: a };
36
+ }
37
+ return { kind: 'literal', value: a };
38
+ }
24
39
  // ---------------------------------------------------------------------------
25
40
  // Arithmetic
26
41
  // ---------------------------------------------------------------------------
27
42
  export function divide(a, b) {
28
- return expr(() => `(${resolveArg(a)}) / (${resolveArg(b)})`);
43
+ return expr({ kind: 'binary', operator: 'divide', left: resolveExpression(a), right: resolveExpression(b) }, () => `(${resolveArg(a)}) / (${resolveArg(b)})`);
29
44
  }
30
45
  export function multiply(a, b) {
31
- return expr(() => `(${resolveArg(a)}) * (${resolveArg(b)})`);
46
+ return expr({ kind: 'binary', operator: 'multiply', left: resolveExpression(a), right: resolveExpression(b) }, () => `(${resolveArg(a)}) * (${resolveArg(b)})`);
32
47
  }
33
48
  export function subtract(a, b) {
34
- return expr(() => `(${resolveArg(a)}) - (${resolveArg(b)})`);
49
+ return expr({ kind: 'binary', operator: 'subtract', left: resolveExpression(a), right: resolveExpression(b) }, () => `(${resolveArg(a)}) - (${resolveArg(b)})`);
35
50
  }
36
51
  export function add(a, b) {
37
- return expr(() => `(${resolveArg(a)}) + (${resolveArg(b)})`);
52
+ return expr({ kind: 'binary', operator: 'add', left: resolveExpression(a), right: resolveExpression(b) }, () => `(${resolveArg(a)}) + (${resolveArg(b)})`);
38
53
  }
39
54
  // ---------------------------------------------------------------------------
40
55
  // Null handling
41
56
  // ---------------------------------------------------------------------------
42
57
  export function nullIfZero(a) {
43
- return expr(() => `NULLIF(${resolveArg(a)}, 0)`);
58
+ return expr({ kind: 'function', name: 'nullIfZero', args: [resolveExpression(a)] }, () => `NULLIF(${resolveArg(a)}, 0)`);
44
59
  }
45
60
  export function coalesce(a, fallback) {
46
61
  const fb = typeof fallback === 'number' ? String(fallback) : resolveArg(fallback);
47
- return expr(() => `COALESCE(${resolveArg(a)}, ${fb})`);
62
+ return expr({ kind: 'function', name: 'coalesce', args: [resolveExpression(a), resolveExpression(fallback)] }, () => `COALESCE(${resolveArg(a)}, ${fb})`);
48
63
  }
49
64
  // ---------------------------------------------------------------------------
50
65
  // Rounding
51
66
  // ---------------------------------------------------------------------------
52
67
  export function round(a, decimals = 0) {
53
- return expr(() => `ROUND(${resolveArg(a)}, ${decimals})`);
68
+ return expr({ kind: 'function', name: 'round', args: [resolveExpression(a), resolveExpression(decimals)] }, () => `ROUND(${resolveArg(a)}, ${decimals})`);
54
69
  }
55
70
  export function floor(a) {
56
- return expr(() => `FLOOR(${resolveArg(a)})`);
71
+ return expr({ kind: 'function', name: 'floor', args: [resolveExpression(a)] }, () => `FLOOR(${resolveArg(a)})`);
57
72
  }
58
73
  export function ceil(a) {
59
- return expr(() => `CEIL(${resolveArg(a)})`);
74
+ return expr({ kind: 'function', name: 'ceil', args: [resolveExpression(a)] }, () => `CEIL(${resolveArg(a)})`);
60
75
  }
@@ -0,0 +1,5 @@
1
+ import type { SemanticBackend } from './semantic-plan.js';
2
+ export type InMemoryTable = Array<Record<string, unknown>>;
3
+ export type InMemoryTables = Record<string, InMemoryTable>;
4
+ export declare function createInMemoryBackend(tables: InMemoryTables): SemanticBackend;
5
+ //# sourceMappingURL=in-memory-backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"in-memory-backend.d.ts","sourceRoot":"","sources":["../src/in-memory-backend.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAGV,eAAe,EAIhB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,MAAM,aAAa,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAC3D,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AA2K3D,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,GAAG,eAAe,CAmE7E"}